wokkel-0.7.1/0000775000175000017500000000000012074346436013616 5ustar ralphmralphm00000000000000wokkel-0.7.1/wokkel.egg-info/0000775000175000017500000000000012074346436016604 5ustar ralphmralphm00000000000000wokkel-0.7.1/wokkel.egg-info/SOURCES.txt0000664000175000017500000000243712074346436020476 0ustar ralphmralphm00000000000000LICENSE MANIFEST.in NEWS README setup.py doc/examples/echo_server.tac doc/examples/muc_client.tac doc/examples/ping_component.tac doc/examples/ping_s2s.tac doc/examples/ping_server.tac doc/examples/pinger.py doc/examples/pinger_client.tac doc/examples/pinger_component.tac doc/examples/pinger_s2s.tac doc/examples/router.tac twisted/plugins/server.py wokkel/__init__.py wokkel/client.py wokkel/compat.py wokkel/component.py wokkel/componentservertap.py wokkel/data_form.py wokkel/delay.py wokkel/disco.py wokkel/formats.py wokkel/generic.py wokkel/iwokkel.py wokkel/muc.py wokkel/ping.py wokkel/pubsub.py wokkel/server.py wokkel/shim.py wokkel/subprotocols.py wokkel/xmppim.py wokkel.egg-info/PKG-INFO wokkel.egg-info/SOURCES.txt wokkel.egg-info/dependency_links.txt wokkel.egg-info/not-zip-safe wokkel.egg-info/requires.txt wokkel.egg-info/top_level.txt wokkel/test/__init__.py wokkel/test/helpers.py wokkel/test/test_client.py wokkel/test/test_compat.py wokkel/test/test_component.py wokkel/test/test_data_form.py wokkel/test/test_delay.py wokkel/test/test_disco.py wokkel/test/test_generic.py wokkel/test/test_iwokkel.py wokkel/test/test_muc.py wokkel/test/test_ping.py wokkel/test/test_pubsub.py wokkel/test/test_server.py wokkel/test/test_shim.py wokkel/test/test_subprotocols.py wokkel/test/test_xmppim.pywokkel-0.7.1/wokkel.egg-info/requires.txt0000664000175000017500000000004112074346436021177 0ustar ralphmralphm00000000000000Twisted >= 10.0.0 python-dateutilwokkel-0.7.1/wokkel.egg-info/not-zip-safe0000664000175000017500000000000111754447235021035 0ustar ralphmralphm00000000000000 wokkel-0.7.1/wokkel.egg-info/PKG-INFO0000664000175000017500000000033212074346436017677 0ustar ralphmralphm00000000000000Metadata-Version: 1.0 Name: wokkel Version: 0.7.1 Summary: Twisted Jabber support library Home-page: http://wokkel.ik.nu/ Author: Ralph Meijer Author-email: ralphm@ik.nu License: MIT Description: UNKNOWN Platform: any wokkel-0.7.1/wokkel.egg-info/top_level.txt0000664000175000017500000000000712074346436021333 0ustar ralphmralphm00000000000000wokkel wokkel-0.7.1/wokkel.egg-info/dependency_links.txt0000664000175000017500000000000112074346436022652 0ustar ralphmralphm00000000000000 wokkel-0.7.1/twisted/0000775000175000017500000000000012074346436015301 5ustar ralphmralphm00000000000000wokkel-0.7.1/twisted/plugins/0000775000175000017500000000000012074346436016762 5ustar ralphmralphm00000000000000wokkel-0.7.1/twisted/plugins/server.py0000775000175000017500000000042711653744106020646 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. from twisted.application.service import ServiceMaker WokkelXMPPComponentServer = ServiceMaker( "XMPP Component Server", "wokkel.componentservertap", "An XMPP Component Server", "wokkel-component-server") wokkel-0.7.1/setup.py0000775000175000017500000000251512074331563015331 0ustar ralphmralphm00000000000000#!/usr/bin/env python # Copyright (c) Ralph Meijer. # See LICENSE for details. from setuptools import setup # Make sure 'twisted' doesn't appear in top_level.txt try: from setuptools.command import egg_info egg_info.write_toplevel_names except (ImportError, AttributeError): pass else: def _top_level_package(name): return name.split('.', 1)[0] def _hacked_write_toplevel_names(cmd, basename, filename): pkgs = dict.fromkeys( [_top_level_package(k) for k in cmd.distribution.iter_distribution_names() if _top_level_package(k) != "twisted" ] ) cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n') egg_info.write_toplevel_names = _hacked_write_toplevel_names setup(name='wokkel', version='0.7.1', description='Twisted Jabber support library', author='Ralph Meijer', author_email='ralphm@ik.nu', maintainer_email='ralphm@ik.nu', url='http://wokkel.ik.nu/', license='MIT', platforms='any', packages=[ 'wokkel', 'wokkel.test', 'twisted.plugins', ], package_data={'twisted.plugins': ['twisted/plugins/server.py']}, zip_safe=False, install_requires=[ 'Twisted >= 10.0.0', 'python-dateutil', ], ) wokkel-0.7.1/MANIFEST.in0000664000175000017500000000012211707274572015352 0ustar ralphmralphm00000000000000include NEWS include LICENSE include doc/examples/*.py include doc/examples/*.tac wokkel-0.7.1/setup.cfg0000664000175000017500000000007312074346436015437 0ustar ralphmralphm00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 wokkel-0.7.1/PKG-INFO0000664000175000017500000000033212074346436014711 0ustar ralphmralphm00000000000000Metadata-Version: 1.0 Name: wokkel Version: 0.7.1 Summary: Twisted Jabber support library Home-page: http://wokkel.ik.nu/ Author: Ralph Meijer Author-email: ralphm@ik.nu License: MIT Description: UNKNOWN Platform: any wokkel-0.7.1/doc/0000775000175000017500000000000012074346436014363 5ustar ralphmralphm00000000000000wokkel-0.7.1/doc/examples/0000775000175000017500000000000012074346436016201 5ustar ralphmralphm00000000000000wokkel-0.7.1/doc/examples/pinger_s2s.tac0000664000175000017500000000234112025346064020736 0ustar ralphmralphm00000000000000""" An XMPP Ping client as a standalone server via s2s. This ping client accepts and initiates server-to-server connections using dialback and listens on C{127.0.1.1} with the domain set to the default hostname of this machine. """ import socket from twisted.application import service, strports from twisted.words.protocols.jabber.jid import JID from wokkel import component, server from pinger import Pinger # Configuration parameters S2S_PORT = 'tcp:5269:interface=127.0.1.1' SECRET = 'secret' DOMAIN = socket.gethostname() OTHER_DOMAIN = 'localhost' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("Pinger Server") router = component.Router() serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET) serverService.logTraffic = LOG_TRAFFIC s2sFactory = server.XMPPS2SServerFactory(serverService) s2sFactory.logTraffic = LOG_TRAFFIC s2sService = strports.service(S2S_PORT, s2sFactory) s2sService.setServiceParent(application) pingerComponent = component.InternalComponent(router, DOMAIN) pingerComponent.logTraffic = LOG_TRAFFIC pingerComponent.setServiceParent(application) pingerHandler = Pinger(JID(OTHER_DOMAIN), JID(DOMAIN)) pingerHandler.setHandlerParent(pingerComponent) wokkel-0.7.1/doc/examples/pinger_client.tac0000644000175000017500000000134311322111274021474 0ustar ralphmralphm00000000000000""" An XMPP Ping client as an XMPP client. This pinger client logs in as C{pinger@example.org}. """ from twisted.application import service from twisted.words.protocols.jabber.jid import JID from wokkel import client from pinger import Pinger # Configuration parameters THIS_JID = JID('pinger@example.org') OTHER_JID = JID('ping.example.com') SECRET = 'secret' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("Pinger Component") pingerClient = client.XMPPClient(THIS_JID, SECRET) pingerClient.logTraffic = LOG_TRAFFIC pingerClient.setServiceParent(application) pingerClient.send('') # Hello, OpenFire! pingerHandler = Pinger(OTHER_JID) pingerHandler.setHandlerParent(pingerClient) wokkel-0.7.1/doc/examples/pinger_component.tac0000644000175000017500000000143011322111274022215 0ustar ralphmralphm00000000000000""" An XMPP Ping client as an external server-side component. This pinger client assumes the domain C{pinger}. """ from twisted.application import service from twisted.words.protocols.jabber.jid import JID from wokkel import component from pinger import Pinger # Configuration parameters EXT_HOST = 'localhost' EXT_PORT = 5347 SECRET = 'secret' DOMAIN = 'pinger' OTHER_DOMAIN = 'ping' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("Pinger Component") router = component.Router() pingerComponent = component.Component(EXT_HOST, EXT_PORT, DOMAIN, SECRET) pingerComponent.logTraffic = LOG_TRAFFIC pingerComponent.setServiceParent(application) pingerHandler = Pinger(JID(OTHER_DOMAIN), JID(DOMAIN)) pingerHandler.setHandlerParent(pingerComponent) wokkel-0.7.1/doc/examples/ping_component.tac0000644000175000017500000000125611322111274021674 0ustar ralphmralphm00000000000000""" An XMPP Ping server as an external server-side component. This ping server assumes the domain C{'ping'}. """ from twisted.application import service from wokkel import component from wokkel.ping import PingHandler # Configuration parameters EXT_HOST = 'localhost' EXT_PORT = 5347 SECRET = 'secret' DOMAIN = 'ping' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("Ping Component") router = component.Router() pingComponent = component.Component(EXT_HOST, EXT_PORT, DOMAIN, SECRET) pingComponent.logTraffic = LOG_TRAFFIC pingComponent.setServiceParent(application) pingHandler = PingHandler() pingHandler.setHandlerParent(pingComponent) wokkel-0.7.1/doc/examples/ping_s2s.tac0000664000175000017500000000210212013012404020363 0ustar ralphmralphm00000000000000""" An XMPP Ping server as a standalone server via s2s. This ping responder accepts and initiates server-to-server connections using dialback and listens on C{127.0.0.1} with the domain C{localhost}. """ from twisted.application import service, strports from wokkel import component, server from wokkel.ping import PingHandler # Configuration parameters S2S_PORT = 'tcp:5269:interface=127.0.0.1' SECRET = 'secret' DOMAIN = 'localhost' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("Ping Server") router = component.Router() serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET) serverService.logTraffic = LOG_TRAFFIC s2sFactory = server.XMPPS2SServerFactory(serverService) s2sFactory.logTraffic = LOG_TRAFFIC s2sService = strports.service(S2S_PORT, s2sFactory) s2sService.setServiceParent(application) pingComponent = component.InternalComponent(router, DOMAIN) pingComponent.logTraffic = LOG_TRAFFIC pingComponent.setServiceParent(application) pingHandler = PingHandler() pingHandler.setHandlerParent(pingComponent) wokkel-0.7.1/doc/examples/router.tac0000644000175000017500000000107711322111274020176 0ustar ralphmralphm00000000000000""" A generic XMPP router. This router accepts external server-side component connections on port 5347, but only on 127.0.0.1. """ from twisted.application import service, strports from wokkel import component application = service.Application("XMPP router") router = component.Router() componentServerFactory = component.XMPPComponentServerFactory(router) componentServerFactory.logTraffic = True componentServer = strports.service('tcp:5347:interface=127.0.0.1', componentServerFactory) componentServer.setServiceParent(application) wokkel-0.7.1/doc/examples/echo_server.tac0000644000175000017500000000716711322111274021170 0ustar ralphmralphm00000000000000""" An XMPP echo server as a standalone server via s2s. This echo server accepts and initiates server-to-server connections using dialback and listens on C{127.0.0.1} with the domain C{localhost}. It will accept subscription requests for any potential entity at the domain and send back messages sent to it. """ from twisted.application import service, strports from twisted.words.protocols.jabber.xmlstream import toResponse from wokkel import component, server, xmppim # Configuration parameters S2S_PORT = 'tcp:5269:interface=127.0.0.1' SECRET = 'secret' DOMAIN = 'localhost' LOG_TRAFFIC = True # Protocol handlers class PresenceAcceptingHandler(xmppim.PresenceProtocol): """ Presence accepting XMPP subprotocol handler. This handler blindly accepts incoming presence subscription requests, confirms unsubscription requests and responds to presence probes. Note that this handler does not remember any contacts, so it will not send presence when starting. """ def subscribedReceived(self, presence): """ Subscription approval confirmation was received. This is just a confirmation. Don't respond. """ pass def unsubscribedReceived(self, presence): """ Unsubscription confirmation was received. This is just a confirmation. Don't respond. """ pass def subscribeReceived(self, presence): """ Subscription request was received. Always grant permission to see our presence. """ self.subscribed(recipient=presence.sender, sender=presence.recipient) self.available(recipient=presence.sender, status=u"I'm here", sender=presence.recipient) def unsubscribeReceived(self, presence): """ Unsubscription request was received. Always confirm unsubscription requests. """ self.unsubscribed(recipient=presence.sender, sender=presence.recipient) def probeReceived(self, presence): """ A presence probe was received. Always send available presence to whoever is asking. """ self.available(recipient=presence.sender, status=u"I'm here", sender=presence.recipient) class EchoHandler(xmppim.MessageProtocol): """ Message echoing XMPP subprotocol handler. """ def onMessage(self, message): """ Called when a message stanza was received. """ # Ignore error messages if message.getAttribute('type') == 'error': return # Echo incoming messages, if they have a body. if message.body and unicode(message.body): response = toResponse(message, message.getAttribute('type')) response.addElement('body', content=unicode(message.body)) self.send(response) # Set up the Twisted application application = service.Application("Ping Server") router = component.Router() serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET) serverService.logTraffic = LOG_TRAFFIC s2sFactory = server.XMPPS2SServerFactory(serverService) s2sFactory.logTraffic = LOG_TRAFFIC s2sService = strports.service(S2S_PORT, s2sFactory) s2sService.setServiceParent(application) echoComponent = component.InternalComponent(router, DOMAIN) echoComponent.logTraffic = LOG_TRAFFIC echoComponent.setServiceParent(application) presenceHandler = PresenceAcceptingHandler() presenceHandler.setHandlerParent(echoComponent) echoHandler = EchoHandler() echoHandler.setHandlerParent(echoComponent) wokkel-0.7.1/doc/examples/pinger.py0000644000175000017500000000101711322111274020015 0ustar ralphmralphm00000000000000""" An XMPP subprotocol handler that acts as an XMPP Ping pinger. """ from wokkel.ping import PingClientProtocol class Pinger(PingClientProtocol): """ I send a ping as soon as I have a connection. """ def __init__(self, entity, sender=None): self.entity = entity self.sender = sender def connectionInitialized(self): def cb(response): print "*** Pong ***" print "*** Ping ***" d = self.ping(self.entity, sender=self.sender) d.addCallback(cb) wokkel-0.7.1/doc/examples/ping_server.tac0000644000175000017500000000202611322111274021174 0ustar ralphmralphm00000000000000""" An XMPP Ping server as a standalone server with external component service. This ping responder server uses the C{ping} domain, and also accepts External Component connections on port C{5347}, but only on C{127.0.0.1}. """ from twisted.application import service, strports from wokkel import component from wokkel.ping import PingHandler # Configuration parameters EXT_PORT = 'tcp:5347:interface=127.0.0.1' SECRET = 'secret' DOMAIN = 'ping' LOG_TRAFFIC = True # Set up the Twisted application application = service.Application("XMPP Ping Server") router = component.Router() componentServerFactory = component.XMPPComponentServerFactory(router, SECRET) componentServerFactory.logTraffic = LOG_TRAFFIC componentServer = strports.service(EXT_PORT, componentServerFactory) componentServer.setServiceParent(application) pingComponent = component.InternalComponent(router, DOMAIN) pingComponent.logTraffic = LOG_TRAFFIC pingComponent.setServiceParent(application) pingHandler = PingHandler() pingHandler.setHandlerParent(pingComponent) wokkel-0.7.1/doc/examples/muc_client.tac0000664000175000017500000000607411707004455021015 0ustar ralphmralphm00000000000000""" An XMPP MUC client. This XMPP Client logs in as C{user@example.org}, joins the room C{'room@muc.example.org'} using the nick C{'greeter'} and responds to greetings addressed to it. If another occupant writes C{'greeter: hello'}, it will return the favor. This example uses L{MUCClient} instead of the protocol-only L{MUCClientProtocol} so that it can hook into its C{receivedGroupChat}. L{MUCClient} implements C{groupChatReceived} and makes a distinction between messages setting the subject, messages that a part of the room's conversation history, and 'live' messages. In this case, we only want to inspect and respond to the 'live' messages. """ from twisted.application import service from twisted.python import log from twisted.words.protocols.jabber.jid import JID from wokkel.client import XMPPClient from wokkel.muc import MUCClient # Configuration parameters THIS_JID = JID('user@example.org') ROOM_JID = JID('room@muc.example.org') NICK = u'greeter' SECRET = 'secret' LOG_TRAFFIC = True class MUCGreeter(MUCClient): """ I join a room and respond to greetings. """ def __init__(self, roomJID, nick): MUCClient.__init__(self) self.roomJID = roomJID self.nick = nick def connectionInitialized(self): """ Once authorized, join the room. If the join action causes a new room to be created, the room will be locked until configured. Here we will just accept the default configuration by submitting an empty form using L{configure}, which usually results in a public non-persistent room. Alternatively, you would use L{getConfiguration} to retrieve the configuration form, and then submit the filled in form with the required settings using L{configure}, possibly after presenting it to an end-user. """ def joinedRoom(room): if room.locked: # Just accept the default configuration. return self.configure(room.roomJID, {}) MUCClient.connectionInitialized(self) d = self.join(self.roomJID, self.nick) d.addCallback(joinedRoom) d.addCallback(lambda _: log.msg("Joined room")) d.addErrback(log.err, "Join failed") def receivedGroupChat(self, room, user, message): """ Called when a groupchat message was received. Check if the message was addressed to my nick and if it said C{'hello'}. Respond by sending a message to the room addressed to the sender. """ if message.body.startswith(self.nick + u":"): nick, text = message.body.split(':', 1) text = text.strip().lower() if text == u'hello': body = u"%s: Hi!" % (user.nick) self.groupChat(self.roomJID, body) # Set up the Twisted application application = service.Application("MUC Client") client = XMPPClient(THIS_JID, SECRET) client.logTraffic = LOG_TRAFFIC client.setServiceParent(application) mucHandler = MUCGreeter(ROOM_JID, NICK) mucHandler.setHandlerParent(client) wokkel-0.7.1/NEWS0000664000175000017500000002224012074331547014312 0ustar ralphmralphm000000000000000.7.1 (2013-01-12) ================== Features -------- - wokkel.generic.Request.parseRequest is a new convenience hook for parsing the payload of incoming requests using fromElement. - wokkel.xmppim.RosterItem can now represent item removals and has methods for XML (de-)serialization (#71). - wokkel.xmppim.RosterRequest is a new class to represent roster request stanzas (#71). - wokkel.xmppim.RosterClientProtocol.getRoster now returns the roster indexed by JID (#71). - wokkel.xmppim.RosterClientProtocol uses the new RosterRequest for sending outgoing requests, using the new request semantics (#71). - wokkel.xmppim.RosterClientProtocol uses the new RosterRequest to provide access to addressing and roster version information in the new callbacks for roster pushes (#71). - wokkel.xmppim.RosterPushIgnored can be raised for unwanted roster pushes (#71). - wokkel.xmppim.RosterClientProtocol and RosterRequest now support roster versioning. - With the new wokkel.xmppim.RosterClientProtocol.setItem roster items can be added or updated (#56). Fixes ----- - wokkel.component.Component now reconnects if first attempt failed (#75). - wokkel.xmppim.RosterClientProtocol now properly checks sender addresses for roster pushes (#71). - Make sure twistd plugins are installed properly (#76). - wokkel.component.Router.route now sends back an error if there is no known route to the stanza's destination. - Properly encode IDN domain names for establishing client and server connections. This resolves an issue with Twisted 12.3.0 that made it impossible to initiate any connection using Wokkel (#77). Deprecations ------------ - wokkel.xmppim.RosterItem.jid is deprecated in favor of entity (#71). - wokkel.xmppim.RosterItem.ask is deprecated in favor of pendingOut (#71). - wokkel.xmppim.RosterClientProtocol.onRosterSet is deprecated in favor of setReceived (#71). - wokkel.xmppim.RosterClientProtocol.onRosterRemove is deprecated in favor of removeReceived (#71). 0.7.0 (2012-01-23) ================== Features -------- - Added method wokkel.data_form.Form.typeCheck for type checking incoming Data Forms submissions against field definitions. - Added method wokkel.data_form.Form.makeFields to add fields from a dictionary mapping field names to values. - Added public function wokkel.data_form.findForm for extracting Data Forms from stanzas. - PubSubRequest.options is now a wokkel.data_form.Form. - wokkel.data_form.Form can now be used as a read-only dictionary. - Added support for configuration options in Publish-Subscribe node create requests. - Added support for subscription options in Publish-Subscribe subscribe requests (#63). - Added support for Publish Subscribe subscription identifiers. - wokkel.pubsub.Item can now be used to send out notifications, too. - Added a twistd plugin to set up a basic XMPP server that accepts component connections and provides server-to-server (dialback) connectivity. - Added support for managing affiliations of Publish-Subscribe nodes, server-side. - Added iq request (set/get) tracking to StreamManager and provide a new base class for such requests: wokkel.generic.Request. Unlike twisted.words.protocols.jabber.xmlstream.IQ, Such requests can be queued until the connection is initialized, with timeouts running from the moment `request` was called (instead of when it was sent over the wire). - Added support for Delayed Delivery information formats. - Added support for XMPP Multi-User Chat, client side (#24). Fixes ----- - XMPP Ping handler now marks incoming ping requests as handled, so the FallbackHandler doesn't respond, too. (#66) - Incorporate Twisted changes for component password hashes. - Completed test coverage for Data Forms. - Made sure Data Forms field labels don't get overwritten (#60). - Service Discovery identity is now reported correctly for legacy PubSubService use (#64). - Various smaller Service Discovery fixes for PubSubService. - Completed test coverage for Service Discovery support. - Publish Subscribe events with stanza type error are now ignored (#69). - Publish Subscribe requests with multiple 'verbs' are now properly parsed (#72). - Publish Subscribe requests that have no legacy support in PubSubService will now result in a feature-not-implemented error (#70). - Publish Subscribe subscription elements now have the correct namespace when sent out. - Incorporated Twisted changes for passing on a reason Failure upon stream disconnect. - Fixed race condition and nesting issues when adding subprotocol handlers to their StreamManager (#48). - Reimplemented Service Discovery requests using new Request class. By reusing common code, this fixes a problem with requests without addressing (#73). Deprecations ------------ - wokkel.compat.BootstrapMixin is deprecated in favor of twisted.words.xish.xmlstream.BootstrapMixin (Twisted 8.2.0). - wokkel.compat.XmlStreamServerFactory is deprecated in favor of twisted.words.protocols.jabber.xmlstream.XmlStreamServerFactory (Twisted 8.2.0). - wokkel.iwokkel.IXMPPHandler is deprecated in favor of twisted.words.protocols.jabber.ijabber.IXMPPHandler (Twisted 8.1.0). - wokkel.iwokkel.IXMPPHandlerCollection is deprecated in favor of twisted.words.protocols.jabber.ijabber.IXMPPHandlerCollection (Twisted 8.1.0). - wokkel.subprotocols.XMPPHandlerCollection is deprecated in favor of twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection (Twisted 8.1.0). 0.6.3 (2009-08-20) ================== Features -------- - Add a jid attribute to XMPPClient (#18). - Add a better presence protocol handler PresenceProtocol. This handler is also useful for component or in-server use. Fixes ----- - Use fallback port 5222 for failed SRV lookups for clients (#26). 0.6.2 (2009-07-08) ================== Features -------- - Add support for XMPP Ping (XEP-0199), doubling as example protocol handler (#55). - Provide examples for setting up clients, components and servers (#55). - Make Service Discovery support accept non-deferred results from getDiscoInfo and getDiscoItems (#55). 0.6.1 (2009-07-06) ================== Features -------- - Add an optional sender parameter for Service Discovery requests (#52). Fixes: ------ - Fix regression in DeferredClientFactory (#51). - Make IQ timeouts work with InternalComponent (#53). 0.6.0 (2009-04-22) ================== Features -------- - Server-to-server support, based on the dialback protocol (#33). - Enhancement to InternalProtocol to support multiple domains (#43). - Publish-subscribe request abstraction (#45). - Publish-subscribe abstraction to implement a node in code (#47). - Enhancement to PubSubClient to send requests from a specific JID (#46). Fixes ----- - Remove type interpretation in Data Forms field parsing code (#44). 0.5.0 (2009-04-07) ================== This release drops support for Twisted versions older than 8.0, including Twisted 2.5 / Twisted Words 0.5. Features -------- - Support for sending and receiving Publish-Subscribe node delete notifications with redirect. - Service Discovery client support, including an overhaul of disco data classes (#28). - Initial support for building XMPP servers has been added: - XmlStreamServerFactory has been backported from Twisted Words (#29). - An XMPP router has been added (#30). - A server-side component authenticator has been added (#30). - A new server-side component service, that connects to a router within the same process, was added (#31). Fixes ----- - Publish-Subscribe subscriptions requests work again (#22). - Publish-Subscribe delete node requests now have the correct namespace (#27). - NodeIDs in Service Discovery requests are now returned in responses (#7). - The presence of stanzaType in toResponse is now checked correctly (#34). - Data Form fields are now rendered depending on form type (#40). - Data Form type checking issues were addressed (#41). - Some compatibility fixes for Twisted 8.0 and 8.1. - Various other fixes (#37, #42) and tracking changes to code already in Twisted. 0.4.0 (2008-08-05) ================== - Refactoring of Data Forms support (#13). - Added support for Stanza Headers and Internet Metadata (SHIM) (#14). - API change for PubSubClient's methods called upon event reception (#14). - Added client-side support for removing roster items. - Implement type checking for data forms (#15). - Added support for publish-subscribe collections: - Correct handling for the root node (empty node identifier). - Send out SHIM 'Collection' header when appropriate. - New Subscription class for working with subscriptions. - API change for PubSubService: - The subscribe method returns a deferred that fires a Subscription - The subscriptions method returns a deferred that fires a list of Subscriptions. - notifyPublish's notifications parameter now expects a list of tuples that includes a list of subscriptions. - Added PubSubService.notifyDelete to allow sending out node deletion notifications. 0.3.1 (2008-04-22) ================== - Fix broken version request handler. 0.3.0 (2008-04-21) ================== First release. wokkel-0.7.1/LICENSE0000664000175000017500000000204611707274572014630 0ustar ralphmralphm00000000000000Copyright (c) 2003-2012 Ralph Meijer. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wokkel-0.7.1/wokkel/0000775000175000017500000000000012074346436015112 5ustar ralphmralphm00000000000000wokkel-0.7.1/wokkel/formats.py0000775000175000017500000000662411707274572017155 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. NS_MOOD = 'http://jabber.org/protocol/mood' NS_TUNE = 'http://jabber.org/protocol/tune' class Mood: """ User mood. This represents a user's mood, as defined in U{XEP-0107}. @ivar value: The mood value. @ivar text: The optional natural-language description of, or reason for the mood. """ def __init__(self, value, text=None): self.value = value self.text = text def fromXml(self, element): """ Get a Mood instance from an XML representation. This class method parses the given XML document into a L{Mood} instances. @param element: The XML mood document. @type element: object providing L{IElement} @return: A L{Mood} instance or C{None} if C{element} was not a mood document or if there was no mood value element. """ if element.uri != NS_MOOD or element.name != 'mood': return None value = None text = None for child in element.elements(): if child.uri != NS_MOOD: continue if child.name == 'text': text = unicode(child) else: value = child.name if value: return Mood(value, text) else: return None fromXml = classmethod(fromXml) class Tune: """ User tune. This represents a user's mood, as defined in U{XEP-0118}. @ivar artist: The artist or performer of the song or piece. @type artist: C{unicode} @ivar length: The duration of the song or piece in seconds. @type length: C{int} @ivar source: The collection (e.g. album) or other source. @type source: C{unicode} @ivar title: The title of the song or piece @type title: C{unicode} @ivar track: A unique identifier for the tune; e.g. the track number within the collection or the specific URI for the object. @type track: C{unicode} @ivar uri: A URI pointing to information about the song, collection, or artist. @type uri: C{str} """ artist = None length = None source = None title = None track = None uri = None def fromXml(self, element): """ Get a Tune instance from an XML representation. This class method parses the given XML document into a L{Tune} instances. @param element: The XML tune document. @type element: object providing L{IElement} @return: A L{Tune} instance or C{None} if C{element} was not a tune document. """ if element.uri != NS_TUNE or element.name != 'tune': return None tune = Tune() for child in element.elements(): if child.uri != NS_TUNE: continue if child.name in ('artist', 'source', 'title', 'track', 'uri'): setattr(tune, child.name, unicode(child)) elif child.name == 'length': try: tune.length = int(unicode(child)) except ValueError: pass return tune fromXml = classmethod(fromXml) wokkel-0.7.1/wokkel/delay.py0000664000175000017500000000625611707265272016573 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_delay -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Delayed Delivery. Support for comunicating Delayed Delivery information as specified by U{XEP-0203} and its predecessor U{XEP-0091}. """ from dateutil.parser import parse from dateutil.tz import tzutc from twisted.words.protocols.jabber.jid import InvalidFormat, JID from twisted.words.xish import domish NS_DELAY = 'urn:xmpp:delay' NS_JABBER_DELAY = 'jabber:x:delay' class Delay(object): """ Delayed Delivery information. Instances of this class represent delayed delivery information that can be parsed from and rendered into both XEP-0203 and legacy XEP-0091 formats. @ivar stamp: The timestamp the stanza was originally sent. @type stamp: L{datetime.datetime} @ivar sender: The optional entity that originally sent the stanza or delayed its delivery. @type sender: L{JID} """ def __init__(self, stamp, sender=None): self.stamp = stamp self.sender = sender def toElement(self, legacy=False): """ Render this instance into a domish Element. @param legacy: If C{True}, use the legacy XEP-0091 format. @type legacy: C{bool} """ if not self.stamp: raise ValueError("stamp is required") if self.stamp.tzinfo is None: raise ValueError("stamp is not offset-aware") if legacy: element = domish.Element((NS_JABBER_DELAY, 'x')) stampFormat = '%Y%m%dT%H:%M:%S' else: element = domish.Element((NS_DELAY, 'delay')) stampFormat = '%Y-%m-%dT%H:%M:%SZ' stamp = self.stamp.astimezone(tzutc()) element['stamp'] = stamp.strftime(stampFormat) if self.sender: element['from'] = self.sender.full() return element @staticmethod def fromElement(element): """ Create an instance from a domish Element. """ try: stamp = parse(element[u'stamp']) # Assume UTC if no timezone was given if stamp.tzinfo is None: stamp = stamp.replace(tzinfo=tzutc()) except (KeyError, ValueError): stamp = None try: sender = JID(element[u'from']) except (KeyError, InvalidFormat): sender = None delay = Delay(stamp, sender) return delay class DelayMixin(object): """ Mixin for parsing delayed delivery information from stanzas. This can be used as a mixin for subclasses of L{wokkel.generic.Stanza} for parsing delayed delivery information. If both XEP-0203 and XEP-0091 formats are present, the former takes precedence. """ delay = None childParsers = { (NS_DELAY, 'delay'): '_childParser_delay', (NS_JABBER_DELAY, 'x'): '_childParser_legacyDelay', } def _childParser_delay(self, element): self.delay = Delay.fromElement(element) def _childParser_legacyDelay(self, element): if not self.delay: self.delay = Delay.fromElement(element) wokkel-0.7.1/wokkel/component.py0000775000175000017500000003151212020400527017452 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_component -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP External Component utilities. """ from twisted.application import service from twisted.internet import reactor from twisted.python import log from twisted.words.protocols.jabber.jid import internJID as JID from twisted.words.protocols.jabber import component, error, xmlstream from twisted.words.xish import domish from wokkel.generic import XmlPipe from wokkel.subprotocols import StreamManager NS_COMPONENT_ACCEPT = 'jabber:component:accept' class Component(StreamManager, service.Service): """ XMPP External Component service. This service is a XMPP stream manager that connects as an External Component to an XMPP server, as described in U{XEP-0114}. """ def __init__(self, host, port, jid, password): self.host = host self.port = port factory = component.componentFactory(jid, password) StreamManager.__init__(self, factory) def _authd(self, xs): """ Called when stream initialization has completed. This replaces the C{send} method of the C{XmlStream} instance that represents the current connection so that outgoing stanzas always have a from attribute set to the JID of the component. """ old_send = xs.send def send(obj): if domish.IElement.providedBy(obj) and \ not obj.getAttribute('from'): obj['from'] = self.xmlstream.thisEntity.full() old_send(obj) xs.send = send StreamManager._authd(self, xs) def initializationFailed(self, reason): """ Called when stream initialization has failed. Stop the service (thereby disconnecting the current stream) and raise the exception. """ self.stopService() reason.raiseException() def startService(self): """ Start the service and connect to the server. """ service.Service.startService(self) self._connection = self._getConnection() def stopService(self): """ Stop the service, close the connection and prevent reconnects. """ service.Service.stopService(self) self.factory.stopTrying() self._connection.disconnect() def _getConnection(self): """ Create a connector that connects to the server. """ return reactor.connectTCP(self.host, self.port, self.factory) class InternalComponent(xmlstream.XMPPHandlerCollection, service.Service): """ Component service that connects directly to a router. Instead of opening a socket to connect to a router, like L{Component}, components of this type connect to a router in the same process. This allows for one-process XMPP servers. @ivar domains: Domains (as C{str}) this component will handle traffic for. @type domains: C{set} """ def __init__(self, router, domain=None): xmlstream.XMPPHandlerCollection.__init__(self) self._router = router self.domains = set() if domain: self.domains.add(domain) self.xmlstream = None def startService(self): """ Create a XML pipe, connect to the router and setup handlers. """ service.Service.startService(self) self._pipe = XmlPipe() self.xmlstream = self._pipe.source for domain in self.domains: self._router.addRoute(domain, self._pipe.sink) for e in self: e.makeConnection(self.xmlstream) e.connectionInitialized() def stopService(self): """ Disconnect from the router and handlers. """ service.Service.stopService(self) for domain in self.domains: self._router.removeRoute(domain, self._pipe.sink) self._pipe = None self.xmlstream = None for e in self: e.connectionLost(None) def addHandler(self, handler): """ Add a new handler and connect it to the stream. """ xmlstream.XMPPHandlerCollection.addHandler(self, handler) if self.xmlstream: handler.makeConnection(self.xmlstream) handler.connectionInitialized() def send(self, obj): """ Send data to the XML stream, so it ends up at the router. """ self.xmlstream.send(obj) class ListenComponentAuthenticator(xmlstream.ListenAuthenticator): """ Authenticator for accepting components. @ivar secret: The shared used to authorized incoming component connections. @type secret: C{unicode}. """ namespace = NS_COMPONENT_ACCEPT def __init__(self, secret): self.secret = secret xmlstream.ListenAuthenticator.__init__(self) def associateWithStream(self, xs): """ Associate the authenticator with a stream. This sets the stream's version to 0.0, because the XEP-0114 component protocol was not designed for XMPP 1.0. """ xs.version = (0, 0) xmlstream.ListenAuthenticator.associateWithStream(self, xs) def streamStarted(self, rootElement): """ Called by the stream when it has started. This examines the default namespace of the incoming stream and whether there is a requested hostname for the component. Then it generates a stream identifier, sends a response header and adds an observer for the first incoming element, triggering L{onElement}. """ xmlstream.ListenAuthenticator.streamStarted(self, rootElement) # Compatibility fix for pre-8.2 implementations of ListenAuthenticator if not self.xmlstream.sid: from twisted.python import randbytes self.xmlstream.sid = randbytes.secureRandom(8).encode('hex') if rootElement.defaultUri != self.namespace: exc = error.StreamError('invalid-namespace') self.xmlstream.sendStreamError(exc) return # self.xmlstream.thisEntity is set to the address the component # wants to assume. if not self.xmlstream.thisEntity: exc = error.StreamError('improper-addressing') self.xmlstream.sendStreamError(exc) return self.xmlstream.sendHeader() self.xmlstream.addOnetimeObserver('/*', self.onElement) def onElement(self, element): """ Called on incoming XML Stanzas. The very first element received should be a request for handshake. Otherwise, the stream is dropped with a 'not-authorized' error. If a handshake request was received, the hash is extracted and passed to L{onHandshake}. """ if (element.uri, element.name) == (self.namespace, 'handshake'): self.onHandshake(unicode(element)) else: exc = error.StreamError('not-authorized') self.xmlstream.sendStreamError(exc) def onHandshake(self, handshake): """ Called upon receiving the handshake request. This checks that the given hash in C{handshake} is equal to a calculated hash, responding with a handshake reply or a stream error. If the handshake was ok, the stream is authorized, and XML Stanzas may be exchanged. """ calculatedHash = xmlstream.hashPassword(self.xmlstream.sid, unicode(self.secret)) if handshake != calculatedHash: exc = error.StreamError('not-authorized', text='Invalid hash') self.xmlstream.sendStreamError(exc) else: self.xmlstream.send('') self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) class Router(object): """ XMPP Server's Router. A router connects the different components of the XMPP service and routes messages between them based on the given routing table. Connected components are trusted to have correct addressing in the stanzas they offer for routing. A route destination of C{None} adds a default route. Traffic for which no specific route exists, will be routed to this default route. @ivar routes: Routes based on the host part of JIDs. Maps host names to the L{EventDispatcher}s that should receive the traffic. A key of C{None} means the default route. @type routes: C{dict} """ def __init__(self): self.routes = {} def addRoute(self, destination, xs): """ Add a new route. The passed XML Stream C{xs} will have an observer for all stanzas added to route its outgoing traffic. In turn, traffic for C{destination} will be passed to this stream. @param destination: Destination of the route to be added as a host name or C{None} for the default route. @type destination: C{str} or C{NoneType} @param xs: XML Stream to register the route for. @type xs: L{EventDispatcher} """ self.routes[destination] = xs xs.addObserver('/*', self.route) def removeRoute(self, destination, xs): """ Remove a route. @param destination: Destination of the route that should be removed. @type destination: C{str}. @param xs: XML Stream to remove the route for. @type xs: L{EventDispatcher} """ xs.removeObserver('/*', self.route) if (xs == self.routes[destination]): del self.routes[destination] def route(self, stanza): """ Route a stanza. @param stanza: The stanza to be routed. @type stanza: L{domish.Element}. """ destination = JID(stanza['to']) if destination.host in self.routes: log.msg("Routing to %s: %r" % (destination.full(), stanza.toXml())) self.routes[destination.host].send(stanza) elif None in self.routes: log.msg("Routing to %s (default route): %r" % (destination.full(), stanza.toXml())) self.routes[None].send(stanza) else: log.msg("No route to %s: %r" % (destination.full(), stanza.toXml())) if stanza.getAttribute('type') not in ('result', 'error'): # No route, send back error exc = error.StanzaError('remote-server-timeout', type='wait') exc.code = '504' response = exc.toResponse(stanza) self.route(response) class XMPPComponentServerFactory(xmlstream.XmlStreamServerFactory): """ XMPP Component Server factory. This factory accepts XMPP external component connections and makes the router service route traffic for a component's bound domain to that component. """ logTraffic = False def __init__(self, router, secret='secret'): self.router = router self.secret = secret def authenticatorFactory(): return ListenComponentAuthenticator(self.secret) xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory) self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self.makeConnection) self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.connectionInitialized) self.serial = 0 def makeConnection(self, xs): """ Called when a component connection was made. This enables traffic debugging on incoming streams. """ xs.serial = self.serial self.serial += 1 def logDataIn(buf): log.msg("RECV (%d): %r" % (xs.serial, buf)) def logDataOut(buf): log.msg("SEND (%d): %r" % (xs.serial, buf)) if self.logTraffic: xs.rawDataInFn = logDataIn xs.rawDataOutFn = logDataOut xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) def connectionInitialized(self, xs): """ Called when a component has succesfully authenticated. Add the component to the routing table and establish a handler for a closed connection. """ destination = xs.thisEntity.host self.router.addRoute(destination, xs) xs.addObserver(xmlstream.STREAM_END_EVENT, self.connectionLost, 0, destination, xs) def onError(self, reason): log.err(reason, "Stream Error") def connectionLost(self, destination, xs, reason): self.router.removeRoute(destination, xs) wokkel-0.7.1/wokkel/componentservertap.py0000775000175000017500000000433011707215356021423 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Component Service. This provides an XMPP server that accepts External Components connections and accepts and initiates server-to-server connections for the specified domain(s). """ from twisted.application import service, strports from twisted.python import usage from twisted.words.protocols.jabber import component from wokkel import server class Options(usage.Options): optParameters = [ ('component-port', None, 'tcp:5347:interface=127.0.0.1', 'Port components connect to'), ('component-secret', None, 'secret', 'Secret components use to connect'), ('server-port', None, 'tcp:5269', 'Port other servers connect to'), ('server-secret', None, None, 'Shared secret for dialback verification'), ] optFlags = [ ('verbose', 'v', 'Log traffic'), ] def __init__(self): usage.Options.__init__(self) self['domains'] = set() def opt_domain(self, domain): """ Domain to accept server connections for. Repeat for more domains. """ self['domains'].add(domain) def postOptions(self): if not self['domains']: raise usage.UsageError('Need at least one domain') def makeService(config): s = service.MultiService() router = component.Router() # Set up the XMPP server service serverService = server.ServerService(router, secret=config['server-secret']) serverService.domains = config['domains'] serverService.logTraffic = config['verbose'] # Hook up XMPP server-to-server service s2sFactory = server.XMPPS2SServerFactory(serverService) s2sFactory.logTraffic = config['verbose'] s2sService = strports.service(config['server-port'], s2sFactory) s2sService.setServiceParent(s) # Hook up XMPP external server-side component service cFactory = component.XMPPComponentServerFactory(router, config['component-secret']) cFactory.logTraffic = config['verbose'] cServer = strports.service(config['component-port'], cFactory) cServer.setServiceParent(s) return s wokkel-0.7.1/wokkel/server.py0000775000175000017500000005645512074267765017023 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_server -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Server-to-Server protocol. This module implements several aspects of XMPP server-to-server communications as described in XMPP Core (RFC 3920). Refer to that document for the meaning of the used terminology. """ # hashlib is new in Python 2.5, try that first. try: from hashlib import sha256 digestmod = sha256 except ImportError: import Crypto.Hash.SHA256 as digestmod sha256 = digestmod.new import hmac from zope.interface import implements from twisted.internet import defer, reactor from twisted.names.srvconnect import SRVConnector from twisted.python import log, randbytes from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream from twisted.words.xish import domish from wokkel.generic import DeferredXmlStreamFactory, XmlPipe, prepareIDNName NS_DIALBACK = 'jabber:server:dialback' def generateKey(secret, receivingServer, originatingServer, streamID): """ Generate a dialback key for server-to-server XMPP Streams. The dialback key is generated using the algorithm described in U{XEP-0185}. The used terminology for the parameters is described in RFC-3920. @param secret: the shared secret known to the Originating Server and Authoritive Server. @type secret: C{str} @param receivingServer: the Receiving Server host name. @type receivingServer: C{str} @param originatingServer: the Originating Server host name. @type originatingServer: C{str} @param streamID: the Stream ID as generated by the Receiving Server. @type streamID: C{str} @return: hexadecimal digest of the generated key. @type: C{str} """ hashObject = sha256() hashObject.update(secret) hashedSecret = hashObject.hexdigest() message = " ".join([receivingServer, originatingServer, streamID]) hash = hmac.HMAC(hashedSecret, message, digestmod=digestmod) return hash.hexdigest() def trapStreamError(xs, observer): """ Trap stream errors. This wraps an observer to catch exceptions. In case of a L{error.StreamError}, it is send over the given XML stream. All other exceptions yield a C{'internal-server-error'} stream error, that is sent over the stream, while the exception is logged. @return: Wrapped observer """ def wrappedObserver(element): try: observer(element) except error.StreamError, exc: xs.sendStreamError(exc) except: log.err() exc = error.StreamError('internal-server-error') xs.sendStreamError(exc) return wrappedObserver class XMPPServerConnector(SRVConnector): def __init__(self, reactor, domain, factory): SRVConnector.__init__(self, reactor, 'xmpp-server', domain, factory) def pickServer(self): host, port = SRVConnector.pickServer(self) if not self.servers and not self.orderedServers: # no SRV record, fall back.. port = 5269 return host, port class DialbackFailed(Exception): pass class OriginatingDialbackInitializer(object): """ Server Dialback Initializer for the Orginating Server. """ implements(ijabber.IInitiatingInitializer) _deferred = None def __init__(self, xs, thisHost, otherHost, secret): self.xmlstream = xs self.thisHost = thisHost self.otherHost = otherHost self.secret = secret def initialize(self): self._deferred = defer.Deferred() self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onStreamError) self.xmlstream.addObserver("/result[@xmlns='%s']" % NS_DIALBACK, self.onResult) key = generateKey(self.secret, self.otherHost, self.thisHost, self.xmlstream.sid) result = domish.Element((NS_DIALBACK, 'result')) result['from'] = self.thisHost result['to'] = self.otherHost result.addContent(key) self.xmlstream.send(result) return self._deferred def onResult(self, result): self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT, self.onStreamError) if result['type'] == 'valid': self.xmlstream.otherEntity = jid.internJID(self.otherHost) self._deferred.callback(None) else: self._deferred.errback(DialbackFailed()) def onStreamError(self, failure): self.xmlstream.removeObserver("/result[@xmlns='%s']" % NS_DIALBACK, self.onResult) self._deferred.errback(failure) class ReceivingDialbackInitializer(object): """ Server Dialback Initializer for the Receiving Server. """ implements(ijabber.IInitiatingInitializer) _deferred = None def __init__(self, xs, thisHost, otherHost, originalStreamID, key): self.xmlstream = xs self.thisHost = thisHost self.otherHost = otherHost self.originalStreamID = originalStreamID self.key = key def initialize(self): self._deferred = defer.Deferred() self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onStreamError) self.xmlstream.addObserver("/verify[@xmlns='%s']" % NS_DIALBACK, self.onVerify) verify = domish.Element((NS_DIALBACK, 'verify')) verify['from'] = self.thisHost verify['to'] = self.otherHost verify['id'] = self.originalStreamID verify.addContent(self.key) self.xmlstream.send(verify) return self._deferred def onVerify(self, verify): self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT, self.onStreamError) if verify['id'] != self.originalStreamID: self.xmlstream.sendStreamError(error.StreamError('invalid-id')) self._deferred.errback(DialbackFailed()) elif verify['to'] != self.thisHost: self.xmlstream.sendStreamError(error.StreamError('host-unknown')) self._deferred.errback(DialbackFailed()) elif verify['from'] != self.otherHost: self.xmlstream.sendStreamError(error.StreamError('invalid-from')) self._deferred.errback(DialbackFailed()) elif verify['type'] == 'valid': self._deferred.callback(None) else: self._deferred.errback(DialbackFailed()) def onStreamError(self, failure): self.xmlstream.removeObserver("/verify[@xmlns='%s']" % NS_DIALBACK, self.onVerify) self._deferred.errback(failure) class XMPPServerConnectAuthenticator(xmlstream.ConnectAuthenticator): """ Authenticator for an outgoing XMPP server-to-server connection. This authenticator connects to C{otherHost} (the Receiving Server) and then initiates dialback as C{thisHost} (the Originating Server) using L{OriginatingDialbackInitializer}. @ivar thisHost: The domain this server connects from (the Originating Server) . @ivar otherHost: The domain of the server this server connects to (the Receiving Server). @ivar secret: The shared secret that is used for verifying the validity of this new connection. """ namespace = 'jabber:server' def __init__(self, thisHost, otherHost, secret): self.thisHost = thisHost self.otherHost = otherHost self.secret = secret xmlstream.ConnectAuthenticator.__init__(self, otherHost) def connectionMade(self): self.xmlstream.thisEntity = jid.internJID(self.thisHost) self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream', NS_DIALBACK: 'db'} xmlstream.ConnectAuthenticator.connectionMade(self) def associateWithStream(self, xs): xmlstream.ConnectAuthenticator.associateWithStream(self, xs) init = OriginatingDialbackInitializer(xs, self.thisHost, self.otherHost, self.secret) xs.initializers = [init] class XMPPServerVerifyAuthenticator(xmlstream.ConnectAuthenticator): """ Authenticator for an outgoing connection to verify an incoming connection. This authenticator connects to C{otherHost} (the Authoritative Server) and then initiates dialback as C{thisHost} (the Receiving Server) using L{ReceivingDialbackInitializer}. @ivar thisHost: The domain this server connects from (the Receiving Server) . @ivar otherHost: The domain of the server this server connects to (the Authoritative Server). @ivar originalStreamID: The stream ID of the incoming connection that is being verified. @ivar key: The key provided by the Receving Server to be verified. """ namespace = 'jabber:server' def __init__(self, thisHost, otherHost, originalStreamID, key): self.thisHost = thisHost self.otherHost = otherHost self.originalStreamID = originalStreamID self.key = key xmlstream.ConnectAuthenticator.__init__(self, otherHost) def connectionMade(self): self.xmlstream.thisEntity = jid.internJID(self.thisHost) self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream', NS_DIALBACK: 'db'} xmlstream.ConnectAuthenticator.connectionMade(self) def associateWithStream(self, xs): xmlstream.ConnectAuthenticator.associateWithStream(self, xs) init = ReceivingDialbackInitializer(xs, self.thisHost, self.otherHost, self.originalStreamID, self.key) xs.initializers = [init] class XMPPServerListenAuthenticator(xmlstream.ListenAuthenticator): """ Authenticator for an incoming XMPP server-to-server connection. This authenticator handles two types of incoming connections. Regular server-to-server connections are from the Originating Server to the Receiving Server, where this server is the Receiving Server. These connections start out by receiving a dialback key, verifying the key with the Authoritative Server, and then accept normal XMPP stanzas. The other type of connections is from a Receiving Server to an Authoritative Server, where this server acts as the Authoritative Server. These connections are used to verify the validity of an outgoing connection from this server. In this case, this server receives a verification request, checks the key and then returns the result. @ivar service: The service that keeps the list of domains we accept connections for. """ namespace = 'jabber:server' def __init__(self, service): xmlstream.ListenAuthenticator.__init__(self) self.service = service def streamStarted(self, rootElement): xmlstream.ListenAuthenticator.streamStarted(self, rootElement) # Compatibility fix for pre-8.2 implementations of ListenAuthenticator if not self.xmlstream.sid: self.xmlstream.sid = randbytes.secureRandom(8).encode('hex') if self.xmlstream.thisEntity: targetDomain = self.xmlstream.thisEntity.host else: targetDomain = self.service.defaultDomain def prepareStream(domain): self.xmlstream.namespace = self.namespace self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream', NS_DIALBACK: 'db'} if domain: self.xmlstream.thisEntity = jid.internJID(domain) try: if xmlstream.NS_STREAMS != rootElement.uri or \ self.namespace != self.xmlstream.namespace or \ ('db', NS_DIALBACK) not in rootElement.localPrefixes.iteritems(): raise error.StreamError('invalid-namespace') if targetDomain and targetDomain not in self.service.domains: raise error.StreamError('host-unknown') except error.StreamError, exc: prepareStream(self.service.defaultDomain) self.xmlstream.sendStreamError(exc) return self.xmlstream.addObserver("//verify[@xmlns='%s']" % NS_DIALBACK, trapStreamError(self.xmlstream, self.onVerify)) self.xmlstream.addObserver("//result[@xmlns='%s']" % NS_DIALBACK, self.onResult) prepareStream(targetDomain) self.xmlstream.sendHeader() if self.xmlstream.version >= (1, 0): features = domish.Element((xmlstream.NS_STREAMS, 'features')) self.xmlstream.send(features) def onVerify(self, verify): try: receivingServer = jid.JID(verify['from']).host originatingServer = jid.JID(verify['to']).host except (KeyError, jid.InvalidFormat): raise error.StreamError('improper-addressing') if originatingServer not in self.service.domains: raise error.StreamError('host-unknown') if (self.xmlstream.otherEntity and receivingServer != self.xmlstream.otherEntity.host): raise error.StreamError('invalid-from') streamID = verify.getAttribute('id', '') key = unicode(verify) calculatedKey = generateKey(self.service.secret, receivingServer, originatingServer, streamID) validity = (key == calculatedKey) and 'valid' or 'invalid' reply = domish.Element((NS_DIALBACK, 'verify')) reply['from'] = originatingServer reply['to'] = receivingServer reply['id'] = streamID reply['type'] = validity self.xmlstream.send(reply) def onResult(self, result): def reply(validity): reply = domish.Element((NS_DIALBACK, 'result')) reply['from'] = result['to'] reply['to'] = result['from'] reply['type'] = validity self.xmlstream.send(reply) def valid(xs): reply('valid') if not self.xmlstream.thisEntity: self.xmlstream.thisEntity = jid.internJID(receivingServer) self.xmlstream.otherEntity = jid.internJID(originatingServer) self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) def invalid(failure): log.err(failure) reply('invalid') receivingServer = result['to'] originatingServer = result['from'] key = unicode(result) d = self.service.validateConnection(receivingServer, originatingServer, self.xmlstream.sid, key) d.addCallbacks(valid, invalid) return d class DeferredS2SClientFactory(DeferredXmlStreamFactory): """ Deferred firing factory for initiating XMPP server-to-server connection. The deferred has its callbacks called upon succesful authentication with the other server. In case of failed authentication or connection, the deferred will have its errbacks called instead. """ logTraffic = False def __init__(self, authenticator): DeferredXmlStreamFactory.__init__(self, authenticator) self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self.onConnectionMade) self.serial = 0 def onConnectionMade(self, xs): xs.serial = self.serial self.serial += 1 def logDataIn(buf): log.msg("RECV (%d): %r" % (xs.serial, buf)) def logDataOut(buf): log.msg("SEND (%d): %r" % (xs.serial, buf)) if self.logTraffic: xs.rawDataInFn = logDataIn xs.rawDataOutFn = logDataOut def initiateS2S(factory): domain = prepareIDNName(factory.authenticator.otherHost) c = XMPPServerConnector(reactor, domain, factory) c.connect() return factory.deferred class XMPPS2SServerFactory(xmlstream.XmlStreamServerFactory): """ XMPP Server-to-Server Server factory. This factory accepts XMPP server-to-server connections. """ logTraffic = False def __init__(self, service): self.service = service def authenticatorFactory(): return XMPPServerListenAuthenticator(service) xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory) self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self.onConnectionMade) self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.onAuthenticated) self.serial = 0 def onConnectionMade(self, xs): """ Called when a server-to-server connection was made. This enables traffic debugging on incoming streams. """ xs.serial = self.serial self.serial += 1 def logDataIn(buf): log.msg("RECV (%d): %r" % (xs.serial, buf)) def logDataOut(buf): log.msg("SEND (%d): %r" % (xs.serial, buf)) if self.logTraffic: xs.rawDataInFn = logDataIn xs.rawDataOutFn = logDataOut xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) def onAuthenticated(self, xs): thisHost = xs.thisEntity.host otherHost = xs.otherEntity.host log.msg("Incoming connection %d from %r to %r established" % (xs.serial, otherHost, thisHost)) xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost, 0, xs) xs.addObserver('/*', self.onElement, 0, xs) def onConnectionLost(self, xs, reason): thisHost = xs.thisEntity.host otherHost = xs.otherEntity.host log.msg("Incoming connection %d from %r to %r disconnected" % (xs.serial, otherHost, thisHost)) def onError(self, reason): log.err(reason, "Stream Error") def onElement(self, xs, element): """ Called when an element was received from one of the connected streams. """ if element.handled: return else: self.service.dispatch(xs, element) class ServerService(object): """ Service for managing XMPP server to server connections. """ logTraffic = False def __init__(self, router, domain=None, secret=None): self.router = router self.defaultDomain = domain self.domains = set() if self.defaultDomain: self.domains.add(self.defaultDomain) if secret is not None: self.secret = secret else: self.secret = randbytes.secureRandom(16).encode('hex') self._outgoingStreams = {} self._outgoingQueues = {} self._outgoingConnecting = set() self.serial = 0 pipe = XmlPipe() self.xmlstream = pipe.source self.router.addRoute(None, pipe.sink) self.xmlstream.addObserver('/*', self.send) def outgoingInitialized(self, xs): thisHost = xs.thisEntity.host otherHost = xs.otherEntity.host log.msg("Outgoing connection %d from %r to %r established" % (xs.serial, thisHost, otherHost)) self._outgoingStreams[thisHost, otherHost] = xs xs.addObserver(xmlstream.STREAM_END_EVENT, lambda _: self.outgoingDisconnected(xs)) if (thisHost, otherHost) in self._outgoingQueues: for element in self._outgoingQueues[thisHost, otherHost]: xs.send(element) del self._outgoingQueues[thisHost, otherHost] def outgoingDisconnected(self, xs): thisHost = xs.thisEntity.host otherHost = xs.otherEntity.host log.msg("Outgoing connection %d from %r to %r disconnected" % (xs.serial, thisHost, otherHost)) del self._outgoingStreams[thisHost, otherHost] def initiateOutgoingStream(self, thisHost, otherHost): """ Initiate an outgoing XMPP server-to-server connection. """ def resetConnecting(_): self._outgoingConnecting.remove((thisHost, otherHost)) if (thisHost, otherHost) in self._outgoingConnecting: return authenticator = XMPPServerConnectAuthenticator(thisHost, otherHost, self.secret) factory = DeferredS2SClientFactory(authenticator) factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.outgoingInitialized) factory.logTraffic = self.logTraffic self._outgoingConnecting.add((thisHost, otherHost)) d = initiateS2S(factory) d.addBoth(resetConnecting) return d def validateConnection(self, thisHost, otherHost, sid, key): """ Validate an incoming XMPP server-to-server connection. """ def connected(xs): # Set up stream for immediate disconnection. def disconnect(_): xs.transport.loseConnection() xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, disconnect) xs.addObserver(xmlstream.INIT_FAILED_EVENT, disconnect) authenticator = XMPPServerVerifyAuthenticator(thisHost, otherHost, sid, key) factory = DeferredS2SClientFactory(authenticator) factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, connected) factory.logTraffic = self.logTraffic d = initiateS2S(factory) return d def send(self, stanza): """ Send stanza to the proper XML Stream. This uses addressing embedded in the stanza to find the correct stream to forward the stanza to. """ otherHost = jid.internJID(stanza["to"]).host thisHost = jid.internJID(stanza["from"]).host if (thisHost, otherHost) not in self._outgoingStreams: # There is no connection with the destination (yet). Cache the # outgoing stanza until the connection has been established. # XXX: If the connection cannot be established, the queue should # be emptied at some point. if (thisHost, otherHost) not in self._outgoingQueues: self._outgoingQueues[(thisHost, otherHost)] = [] self._outgoingQueues[(thisHost, otherHost)].append(stanza) self.initiateOutgoingStream(thisHost, otherHost) else: self._outgoingStreams[(thisHost, otherHost)].send(stanza) def dispatch(self, xs, stanza): """ Send on element to be routed within the server. """ stanzaFrom = stanza.getAttribute('from') stanzaTo = stanza.getAttribute('to') if not stanzaFrom or not stanzaTo: xs.sendStreamError(error.StreamError('improper-addressing')) else: try: sender = jid.internJID(stanzaFrom) jid.internJID(stanzaTo) except jid.InvalidFormat: log.msg("Dropping error stanza with malformed JID") if sender.host != xs.otherEntity.host: xs.sendStreamError(error.StreamError('invalid-from')) else: self.xmlstream.send(stanza) wokkel-0.7.1/wokkel/iwokkel.py0000775000175000017500000010660012074262111017122 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_iwokkel -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Wokkel interfaces. """ __all__ = ['IXMPPHandler', 'IXMPPHandlerCollection', 'IPubSubClient', 'IPubSubService', 'IPubSubResource', 'IMUCClient', 'IMUCStatuses'] from zope.interface import Interface from twisted.python.deprecate import deprecatedModuleAttribute from twisted.python.versions import Version from twisted.words.protocols.jabber.ijabber import IXMPPHandler from twisted.words.protocols.jabber.ijabber import IXMPPHandlerCollection deprecatedModuleAttribute( Version("Wokkel", 0, 7, 0), "Use twisted.words.protocols.jabber.ijabber.IXMPPHandler instead.", __name__, "IXMPPHandler") deprecatedModuleAttribute( Version("Wokkel", 0, 7, 0), "Use twisted.words.protocols.jabber.ijabber.IXMPPHandlerCollection " "instead.", __name__, "IXMPPHandlerCollection") class IDisco(Interface): """ Interface for XMPP service discovery. """ def getDiscoInfo(requestor, target, nodeIdentifier=''): """ Get identity and features from this entity, node. @param requestor: The entity the request originated from. @type requestor: L{JID} @param target: The target entity to which the request is made. @type target: L{JID} @param nodeIdentifier: The optional identifier of the node at this entity to retrieve the identify and features of. The default is C{''}, meaning the root node. @type nodeIdentifier: C{unicode} """ def getDiscoItems(requestor, target, nodeIdentifier=''): """ Get contained items for this entity, node. @param requestor: The entity the request originated from. @type requestor: L{JID} @param target: The target entity to which the request is made. @type target: L{JID} @param nodeIdentifier: The optional identifier of the node at this entity to retrieve the identify and features of. The default is C{''}, meaning the root node. @type nodeIdentifier: C{unicode} """ class IPubSubClient(Interface): def itemsReceived(event): """ Called when an items notification has been received for a node. An item can be an element named C{item} or C{retract}. Respectively, they signal an item being published or retracted, optionally accompanied with an item identifier in the C{id} attribute. @param event: The items event. @type event: L{ItemsEvent} """ def deleteReceived(event): """ Called when a deletion notification has been received for a node. @param event: The items event. @type event: L{ItemsEvent} """ def purgeReceived(event): """ Called when a purge notification has been received for a node. Upon receiving this notification all items associated should be considered retracted. @param event: The items event. @type event: L{ItemsEvent} """ def createNode(service, nodeIdentifier=None): """ Create a new publish subscribe node. @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Optional suggestion for the new node's identifier. If omitted, the creation of an instant node will be attempted. @type nodeIdentifier: C{unicode} @return: a deferred that fires with the identifier of the newly created node. Note that this can differ from the suggested identifier if the publish subscribe service chooses to modify or ignore the suggested identifier. @rtype: L{Deferred} """ def deleteNode(service, nodeIdentifier): """ Delete a node. @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to be deleted. @type nodeIdentifier: C{unicode} @rtype: L{Deferred} """ def subscribe(service, nodeIdentifier, subscriber): """ Subscribe to a node with a given JID. @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to subscribe to. @type nodeIdentifier: C{unicode} @param subscriber: JID to subscribe to the node. @type subscriber: L{JID} @rtype: L{Deferred} """ def unsubscribe(service, nodeIdentifier, subscriber): """ Unsubscribe from a node with a given JID. @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to unsubscribe from. @type nodeIdentifier: C{unicode} @param subscriber: JID to unsubscribe from the node. @type subscriber: L{JID} @rtype: L{Deferred} """ def publish(service, nodeIdentifier, items=[]): """ Publish to a node. Node that the C{items} parameter is optional, because so-called transient, notification-only nodes do not use items and publish actions only signify a change in some resource. @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to publish to. @type nodeIdentifier: C{unicode} @param items: List of item elements. @type items: C{list} of L{Item} @rtype: L{Deferred} """ class IPubSubService(Interface): """ Interface for an XMPP Publish Subscribe Service. All methods that are called as the result of an XMPP request are to return a deferred that fires when the requested action has been performed. Alternatively, exceptions maybe raised directly or by calling C{errback} on the returned deferred. """ def notifyPublish(service, nodeIdentifier, notifications): """ Send out notifications for a publish event. @param service: The entity the notifications will originate from. @type service: L{JID} @param nodeIdentifier: The identifier of the node that was published to. @type nodeIdentifier: C{unicode} @param notifications: The notifications as tuples of subscriber, the list of subscriptions and the list of items to be notified. @type notifications: C{list} of (L{JID}, C{list} of L{Subscription}, C{list} of L{Element}) """ def notifyDelete(service, nodeIdentifier, subscribers, redirectURI=None): """ Send out node deletion notifications. @param service: The entity the notifications will originate from. @type service: L{JID} @param nodeIdentifier: The identifier of the node that was deleted. @type nodeIdentifier: C{unicode} @param subscribers: The subscribers for which a notification should be sent out. @type subscribers: C{list} of L{JID} @param redirectURI: Optional XMPP URI of another node that subscribers are redirected to. @type redirectURI: C{str} """ def publish(requestor, service, nodeIdentifier, items): """ Called when a publish request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to publish to. @type nodeIdentifier: C{unicode} @param items: The items to be published as elements. @type items: C{list} of C{Element} @return: deferred that fires on success. @rtype: L{Deferred} """ def subscribe(requestor, service, nodeIdentifier, subscriber): """ Called when a subscribe request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to subscribe to. @type nodeIdentifier: C{unicode} @param subscriber: The entity to be subscribed. @type subscriber: L{JID} @return: A deferred that fires with a L{Subscription}. @rtype: L{Deferred} """ def unsubscribe(requestor, service, nodeIdentifier, subscriber): """ Called when a subscribe request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to unsubscribe from. @type nodeIdentifier: C{unicode} @param subscriber: The entity to be unsubscribed. @type subscriber: L{JID} @return: A deferred that fires with C{None} when unsubscription has succeeded. @rtype: L{Deferred} """ def subscriptions(requestor, service): """ Called when a subscriptions retrieval request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @return: A deferred that fires with a C{list} of subscriptions as L{Subscription}. @rtype: L{Deferred} """ def affiliations(requestor, service): """ Called when a affiliations retrieval request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @return: A deferred that fires with a C{list} of affiliations as C{tuple}s of (node identifier as C{unicode}, affiliation state as C{str}). The affiliation can be C{'owner'}, C{'publisher'}, or C{'outcast'}. @rtype: L{Deferred} """ def create(requestor, service, nodeIdentifier): """ Called when a node creation request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The suggestion for the identifier of the node to be created. If the request did not include a suggestion for the node identifier, the value is C{None}. @type nodeIdentifier: C{unicode} or C{NoneType} @return: A deferred that fires with a C{unicode} that represents the identifier of the new node. @rtype: L{Deferred} """ def getConfigurationOptions(): """ Retrieve all known node configuration options. The returned dictionary holds the possible node configuration options by option name. The value of each entry represents the specifics for that option in a dictionary: - C{'type'} (C{str}): The option's type (see L{Field}'s doc string for possible values). - C{'label'} (C{unicode}): A human readable label for this option. - C{'options'} (C{dict}): Optional list of possible values for this option. Example:: { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"}, "pubsub#send_last_published_item": {"type": "list-single", "label": "When to send the last published item", "options": { "never": "Never", "on_sub": "When a new subscription is processed"} } } @rtype: C{dict}. """ def getDefaultConfiguration(requestor, service, nodeType): """ Called when a default node configuration request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeType: The type of node for which the configuration is retrieved, C{'leaf'} or C{'collection'}. @type nodeType: C{str} @return: A deferred that fires with a C{dict} representing the default node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or C{bool}. @rtype: L{Deferred} """ def getConfiguration(requestor, service, nodeIdentifier): """ Called when a node configuration retrieval request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to retrieve the configuration from. @type nodeIdentifier: C{unicode} @return: A deferred that fires with a C{dict} representing the node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or C{bool}. @rtype: L{Deferred} """ def setConfiguration(requestor, service, nodeIdentifier, options): """ Called when a node configuration change request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to change the configuration of. @type nodeIdentifier: C{unicode} @return: A deferred that fires with C{None} when the node's configuration has been changed. @rtype: L{Deferred} """ def items(requestor, service, nodeIdentifier, maxItems, itemIdentifiers): """ Called when a items retrieval request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to retrieve items from. @type nodeIdentifier: C{unicode} """ def retract(requestor, service, nodeIdentifier, itemIdentifiers): """ Called when a item retraction request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to retract items from. @type nodeIdentifier: C{unicode} """ def purge(requestor, service, nodeIdentifier): """ Called when a node purge request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to be purged. @type nodeIdentifier: C{unicode} """ def delete(requestor, service, nodeIdentifier): """ Called when a node deletion request has been received. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The entity the request was addressed to. @type service: L{JID} @param nodeIdentifier: The identifier of the node to be delete. @type nodeIdentifier: C{unicode} """ class IPubSubResource(Interface): def locateResource(request): """ Locate a resource that will handle the request. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} """ def getInfo(requestor, service, nodeIdentifier): """ Get node type and meta data. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to request the info for. @type nodeIdentifier: C{unicode} @return: A deferred that fires with a dictionary. If not empty, it must have the keys C{'type'} and C{'meta-data'} to keep respectively the node type and a dictionary with the meta data for that node. @rtype: L{Deferred} """ def getNodes(requestor, service, nodeIdentifier): """ Get all nodes contained by this node. @param requestor: The entity the request originated from. @type requestor: L{JID} @param service: The publish-subscribe service entity. @type service: L{JID} @param nodeIdentifier: Identifier of the node to request the childs for. @type nodeIdentifier: C{unicode} @return: A deferred that fires with a list of child node identifiers. @rtype: L{Deferred} """ def getConfigurationOptions(): """ Retrieve all known node configuration options. The returned dictionary holds the possible node configuration options by option name. The value of each entry represents the specifics for that option in a dictionary: - C{'type'} (C{str}): The option's type (see L{Field}'s doc string for possible values). - C{'label'} (C{unicode}): A human readable label for this option. - C{'options'} (C{dict}): Optional list of possible values for this option. Example:: { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"}, "pubsub#send_last_published_item": {"type": "list-single", "label": "When to send the last published item", "options": { "never": "Never", "on_sub": "When a new subscription is processed"} } } @rtype: C{dict}. """ def publish(request): """ Called when a publish request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: deferred that fires on success. @rtype: L{Deferred} """ def subscribe(request): """ Called when a subscribe request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a L{Subscription}. @rtype: L{Deferred} """ def unsubscribe(request): """ Called when a subscribe request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when unsubscription has succeeded. @rtype: L{Deferred} """ def subscriptions(request): """ Called when a subscriptions retrieval request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{list} of subscriptions as L{Subscription}. @rtype: L{Deferred} """ def affiliations(request): """ Called when a affiliations retrieval request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{list} of affiliations as C{tuple}s of (node identifier as C{unicode}, affiliation state as C{str}). The affiliation can be C{'owner'}, C{'publisher'}, or C{'outcast'}. @rtype: L{Deferred} """ def create(request): """ Called when a node creation request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{unicode} that represents the identifier of the new node. @rtype: L{Deferred} """ def default(request): """ Called when a default node configuration request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{dict} representing the default node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or C{bool}. @rtype: L{Deferred} """ def configureGet(request): """ Called when a node configuration retrieval request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{dict} representing the node configuration. Keys are C{str}s that represent the field name. Values can be of types C{unicode}, C{int} or C{bool}. @rtype: L{Deferred} """ def configureSet(request): """ Called when a node configuration change request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the node's configuration has been changed. @rtype: L{Deferred} """ def items(request): """ Called when a items retrieval request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{list} of L{pubsub.Item}. @rtype: L{Deferred} """ def retract(request): """ Called when a item retraction request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the given items have been retracted. @rtype: L{Deferred} """ def purge(request): """ Called when a node purge request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the node has been purged. @rtype: L{Deferred} """ def delete(request): """ Called when a node deletion request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the node has been deleted. @rtype: L{Deferred} """ def affiliationsGet(request): """ Called when an owner affiliations retrieval request been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with a C{dict} of affiliations with the entity as key (L{JID}) and the affiliation state as value (C{unicode}). The affiliation can be C{u'owner'}, C{u'publisher'}, or C{u'outcast'}. @rtype: L{Deferred} @note: Affiliations are always on the bare JID. An implementation of this method MUST NOT return JIDs with a resource part. """ def affiliationsSet(request): """ Called when a affiliations modify request has been received. @param request: The publish-subscribe request. @type request: L{wokkel.pubsub.PubSubRequest} @return: A deferred that fires with C{None} when the affiliation changes were succesfully processed.. @rtype: L{Deferred} @note: Affiliations are always on the bare JID. The JIDs in L{wokkel.pubsub.PubSubRequest}'s C{affiliations} attribute are already stripped of any resource. """ class IMUCClient(Interface): """ Multi-User Chat Client. A client interface to XEP-045 : http://xmpp.org/extensions/xep-0045.html """ def receivedSubject(room, user, subject): """ The room subject has been received. A subject is received when you join a room and when the subject is changed. @param room: The room the subject was accepted for. @type room: L{muc.Room} @param user: The user that set the subject. @type user: L{muc.User} @param subject: The subject of the given room. @type subject: C{unicode} """ def receivedHistory(room, user, message): """ Past messages from a chat room have been received. This occurs when you join a room. """ def configure(roomJID, options): """ Configure a room. @param roomJID: The room to configure. @type roomJID: L{JID} @param options: A mapping of field names to values, or C{None} to cancel. @type options: C{dict} """ def getConfiguration(roomJID): """ Grab the configuration from the room. This sends an iq request to the room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @return: A deferred that fires with the room's configuration form as a L{data_form.Form} or C{None} if there are no configuration options available. """ def join(roomJID, nick, historyOptions=None, password=None): """ Join a MUC room by sending presence to it. @param roomJID: The JID of the room the entity is joining. @type roomJID: L{JID} @param nick: The nick name for the entitity joining the room. @type nick: C{unicode} @param historyOptions: Options for conversation history sent by the room upon joining. @type historyOptions: L{HistoryOptions} @param password: Optional password for the room. @type password: C{unicode} @return: A deferred that fires when the entity is in the room or an error has occurred. """ def nick(roomJID, nick): """ Change an entity's nick name in a MUC room. See: http://xmpp.org/extensions/xep-0045.html#changenick @param roomJID: The JID of the room, i.e. without a resource. @type roomJID: L{JID} @param nick: The new nick name within the room. @type nick: C{unicode} """ def leave(roomJID): """ Leave a MUC room. See: http://xmpp.org/extensions/xep-0045.html#exit @param roomJID: The Room JID of the room to leave. @type roomJID: L{JID} """ def userJoinedRoom(room, user): """ User has joined a MUC room. This method will need to be modified inorder for clients to do something when this event occurs. @param room: The room the user joined. @type room: L{muc.Room} @param user: The user that joined the room. @type user: L{muc.User} """ def groupChat(roomJID, body): """ Send a groupchat message. """ def chat(occupantJID, body): """ Send a private chat message to a user in a MUC room. See: http://xmpp.org/extensions/xep-0045.html#privatemessage @param occupantJID: The Room JID of the other user. @type occupantJID: L{JID} """ def register(roomJID, options): """ Send a request to register for a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param options: A mapping of field names to values, or C{None} to cancel. @type options: C{dict} """ def subject(roomJID, subject): """ Change the subject of a MUC room. See: http://xmpp.org/extensions/xep-0045.html#subject-mod @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param subject: The subject you want to set. @type subject: C{unicode} """ def voice(roomJID): """ Request voice for a moderated room. @param roomJID: The room jabber/xmpp entity id. @type roomJID: L{JID} """ def history(roomJID, messages): """ Send history to create a MUC based on a one on one chat. See: http://xmpp.org/extensions/xep-0045.html#continue @param roomJID: The room jabber/xmpp entity id. @type roomJID: L{JID} @param messages: The history to send to the room as an ordered list of message, represented by a dictionary with the keys C{'stanza'}, holding the original stanza a L{Element}, and C{'timestamp'} with the timestamp. @type messages: C{list} of L{Element} """ def ban(roomJID, entity, reason=None, sender=None): """ Ban a user from a MUC room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param entity: The bare JID of the entity to be banned. @type entity: L{JID} @param reason: The reason for banning the entity. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ def kick(roomJID, nick, reason=None, sender=None): """ Kick a user from a MUC room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param nick: The occupant to be banned. @type nick: L{JID} or C{unicode} @param reason: The reason given for the kick. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ class IMUCStatuses(Interface): """ Interface for a container of Multi-User Chat status conditions. """ def __contains__(key): """ Return if a status exists in the container. """ def __iter__(): """ Return an iterator over the status codes. """ def __len__(): """ Return the number of status conditions. """ wokkel-0.7.1/wokkel/generic.py0000775000175000017500000002423212074265150017077 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_generic -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Generic XMPP protocol helpers. """ from encodings import idna from zope.interface import implements from twisted.internet import defer, protocol from twisted.python import reflect from twisted.words.protocols.jabber import error, jid, xmlstream from twisted.words.protocols.jabber.xmlstream import toResponse from twisted.words.xish import domish, utility from twisted.words.xish.xmlstream import BootstrapMixin from wokkel.iwokkel import IDisco from wokkel.subprotocols import XMPPHandler IQ_GET = '/iq[@type="get"]' IQ_SET = '/iq[@type="set"]' NS_VERSION = 'jabber:iq:version' VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' def parseXml(string): """ Parse serialized XML into a DOM structure. @param string: The serialized XML to be parsed, UTF-8 encoded. @type string: C{str}. @return: The DOM structure, or C{None} on empty or incomplete input. @rtype: L{domish.Element} """ roots = [] results = [] elementStream = domish.elementStream() elementStream.DocumentStartEvent = roots.append elementStream.ElementEvent = lambda elem: roots[0].addChild(elem) elementStream.DocumentEndEvent = lambda: results.append(roots[0]) elementStream.parse(string) return results and results[0] or None def stripNamespace(rootElement): namespace = rootElement.uri def strip(element): if element.uri == namespace: element.uri = None if element.defaultUri == namespace: element.defaultUri = None for child in element.elements(): strip(child) if namespace is not None: strip(rootElement) return rootElement class FallbackHandler(XMPPHandler): """ XMPP subprotocol handler that catches unhandled iq requests. Unhandled iq requests are replied to with a service-unavailable stanza error. """ def connectionInitialized(self): self.xmlstream.addObserver(IQ_SET, self.iqFallback, -1) self.xmlstream.addObserver(IQ_GET, self.iqFallback, -1) def iqFallback(self, iq): if iq.handled == True: return reply = error.StanzaError('service-unavailable') self.xmlstream.send(reply.toResponse(iq)) class VersionHandler(XMPPHandler): """ XMPP subprotocol handler for XMPP Software Version. This protocol is described in U{XEP-0092}. """ implements(IDisco) def __init__(self, name, version): self.name = name self.version = version def connectionInitialized(self): self.xmlstream.addObserver(VERSION, self.onVersion) def onVersion(self, iq): response = toResponse(iq, "result") query = response.addElement((NS_VERSION, "query")) query.addElement("name", content=self.name) query.addElement("version", content=self.version) self.send(response) iq.handled = True def getDiscoInfo(self, requestor, target, node): info = set() if not node: from wokkel import disco info.add(disco.DiscoFeature(NS_VERSION)) return defer.succeed(info) def getDiscoItems(self, requestor, target, node): return defer.succeed([]) class XmlPipe(object): """ XML stream pipe. Connects two objects that communicate stanzas through an XML stream like interface. Each of the ends of the pipe (sink and source) can be used to send XML stanzas to the other side, or add observers to process XML stanzas that were sent from the other side. XML pipes are usually used in place of regular XML streams that are transported over TCP. This is the reason for the use of the names source and sink for both ends of the pipe. The source side corresponds with the entity that initiated the TCP connection, whereas the sink corresponds with the entity that accepts that connection. In this object, though, the source and sink are treated equally. Unlike Jabber L{XmlStream}s, the sink and source objects are assumed to represent an eternal connected and initialized XML stream. As such, events corresponding to connection, disconnection, initialization and stream errors are not dispatched or processed. @ivar source: Source XML stream. @ivar sink: Sink XML stream. """ def __init__(self): self.source = utility.EventDispatcher() self.sink = utility.EventDispatcher() self.source.send = lambda obj: self.sink.dispatch(obj) self.sink.send = lambda obj: self.source.dispatch(obj) class Stanza(object): """ Abstract representation of a stanza. @ivar sender: The sending entity. @type sender: L{jid.JID} @ivar recipient: The receiving entity. @type recipient: L{jid.JID} """ recipient = None sender = None stanzaKind = None stanzaID = None stanzaType = None def __init__(self, recipient=None, sender=None): self.recipient = recipient self.sender = sender @classmethod def fromElement(Class, element): """ Create a stanza from a L{domish.Element}. """ stanza = Class() stanza.parseElement(element) return stanza def parseElement(self, element): """ Parse the stanza element. This is called with the stanza's element when a L{Stanza} is created using L{fromElement}. It parses the stanza's core attributes (addressing, type and id), strips the namespace from the stanza element for easier transport across streams and passes on child elements for further parsing. Child element parsers are defined by providing a C{childParsers} attribute on a subclass, as a mapping from (URI, name) to the name of the handler on C{self}. C{parseElement} will accumulate C{childParsers} from its class hierarchy, iterate over the child elements and pass it to matching handlers based on the child element's URI and name. The special key of C{None} can be used to pass all child elements to. """ if element.hasAttribute('from'): self.sender = jid.internJID(element['from']) if element.hasAttribute('to'): self.recipient = jid.internJID(element['to']) self.stanzaType = element.getAttribute('type') self.stanzaID = element.getAttribute('id') # Save element stripNamespace(element) self.element = element # accumulate all childHandlers in the class hierarchy of Class handlers = {} reflect.accumulateClassDict(self.__class__, 'childParsers', handlers) for child in element.elements(): try: handler = handlers[child.uri, child.name] except KeyError: try: handler = handlers[None] except KeyError: continue getattr(self, handler)(child) def toElement(self): element = domish.Element((None, self.stanzaKind)) if self.sender is not None: element['from'] = self.sender.full() if self.recipient is not None: element['to'] = self.recipient.full() if self.stanzaType: element['type'] = self.stanzaType if self.stanzaID: element['id'] = self.stanzaID return element class ErrorStanza(Stanza): def parseElement(self, element): Stanza.parseElement(self, element) self.exception = error.exceptionFromStanza(element) class Request(Stanza): """ IQ request stanza. This is a base class for IQ get or set stanzas, to be used with L{wokkel.subprotocols.StreamManager.request}. """ stanzaKind = 'iq' stanzaType = 'get' timeout = None childParsers = {None: 'parseRequest'} def __init__(self, recipient=None, sender=None, stanzaType='get'): Stanza.__init__(self, recipient=recipient, sender=sender) self.stanzaType = stanzaType def parseRequest(self, element): """ Called with the request's child element for parsing. When a request instance is created using L{fromElement}, this method is called with the child element of the iq. Override this method for parsing the request's payload. """ def toElement(self): element = Stanza.toElement(self) if not self.stanzaID: element.addUniqueId() self.stanzaID = element['id'] return element class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory): protocol = xmlstream.XmlStream def __init__(self, authenticator): BootstrapMixin.__init__(self) self.authenticator = authenticator deferred = defer.Deferred() self.deferred = deferred self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback) self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback) def buildProtocol(self, addr): """ Create an instance of XmlStream. A new authenticator instance will be created and passed to the new XmlStream. Registered bootstrap event observers are installed as well. """ xs = self.protocol(self.authenticator) xs.factory = self self.installBootstraps(xs) return xs def clientConnectionFailed(self, connector, reason): self.deferred.errback(reason) def prepareIDNName(name): """ Encode a unicode IDN Domain Name into its ACE equivalent. This will encode the domain labels, separated by allowed dot code points, to their ASCII Compatible Encoding (ACE) equivalent, using punycode. The result is an ASCII byte string of the encoded labels, separated by the standard full stop. """ result = [] labels = idna.dots.split(name) if labels and len(labels[-1]) == 0: trailing_dot = b'.' del labels[-1] else: trailing_dot = b'' for label in labels: result.append(idna.ToASCII(label)) return b'.'.join(result) + trailing_dot wokkel-0.7.1/wokkel/client.py0000775000175000017500000001225712074266455016756 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_client -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Client support. This module holds several improvements on top of Twisted's XMPP support that should probably eventually move there. """ from twisted.application import service from twisted.internet import reactor from twisted.names.srvconnect import SRVConnector from twisted.words.protocols.jabber import client, sasl, xmlstream from wokkel import generic from wokkel.subprotocols import StreamManager class CheckAuthInitializer(object): """ Check what authentication methods are available. """ def __init__(self, xs): self.xmlstream = xs def initialize(self): if (sasl.NS_XMPP_SASL, 'mechanisms') in self.xmlstream.features: inits = [(sasl.SASLInitiatingInitializer, True), (client.BindInitializer, True), (client.SessionInitializer, False)] for initClass, required in inits: init = initClass(self.xmlstream) init.required = required self.xmlstream.initializers.append(init) elif (client.NS_IQ_AUTH_FEATURE, 'auth') in self.xmlstream.features: self.xmlstream.initializers.append( client.IQAuthInitializer(self.xmlstream)) else: raise Exception("No available authentication method found") class HybridAuthenticator(xmlstream.ConnectAuthenticator): """ Initializes an XmlStream connecting to an XMPP server as a Client. This is similar to L{client.XMPPAuthenticator}, but also tries non-SASL autentication. """ namespace = 'jabber:client' def __init__(self, jid, password): xmlstream.ConnectAuthenticator.__init__(self, jid.host) self.jid = jid self.password = password def associateWithStream(self, xs): xmlstream.ConnectAuthenticator.associateWithStream(self, xs) tlsInit = xmlstream.TLSInitiatingInitializer(xs) xs.initializers = [client.CheckVersionInitializer(xs), tlsInit, CheckAuthInitializer(xs)] def HybridClientFactory(jid, password): """ Client factory for XMPP 1.0. This is similar to L{client.XMPPClientFactory} but also tries non-SASL autentication. """ a = HybridAuthenticator(jid, password) return xmlstream.XmlStreamFactory(a) class XMPPClient(StreamManager, service.Service): """ Service that initiates an XMPP client connection. """ def __init__(self, jid, password, host=None, port=5222): self.jid = jid self.domain = generic.prepareIDNName(jid.host) self.host = host self.port = port factory = HybridClientFactory(jid, password) StreamManager.__init__(self, factory) def startService(self): service.Service.startService(self) self._connection = self._getConnection() def stopService(self): service.Service.stopService(self) self.factory.stopTrying() self._connection.disconnect() def _authd(self, xs): """ Called when the stream has been initialized. Save the JID that we were assigned by the server, as the resource might differ from the JID we asked for. This is stored on the authenticator by its constituent initializers. """ self.jid = self.factory.authenticator.jid StreamManager._authd(self, xs) def initializationFailed(self, reason): """ Called when stream initialization has failed. Stop the service (thereby disconnecting the current stream) and raise the exception. """ self.stopService() reason.raiseException() def _getConnection(self): if self.host: return reactor.connectTCP(self.host, self.port, self.factory) else: c = XMPPClientConnector(reactor, self.domain, self.factory) c.connect() return c class DeferredClientFactory(generic.DeferredXmlStreamFactory): def __init__(self, jid, password): authenticator = client.XMPPAuthenticator(jid, password) generic.DeferredXmlStreamFactory.__init__(self, authenticator) self.streamManager = StreamManager(self) def addHandler(self, handler): """ Add a subprotocol handler to the stream manager. """ self.streamManager.addHandler(handler) def removeHandler(self, handler): """ Add a subprotocol handler to the stream manager. """ self.streamManager.removeHandler(handler) class XMPPClientConnector(SRVConnector): def __init__(self, reactor, domain, factory): SRVConnector.__init__(self, reactor, 'xmpp-client', domain, factory) def pickServer(self): host, port = SRVConnector.pickServer(self) if not self.servers and not self.orderedServers: # no SRV record, fall back.. port = 5222 return host, port def clientCreator(factory): domain = generic.prepareIDNName(factory.authenticator.jid.host) c = XMPPClientConnector(reactor, domain, factory) c.connect() return factory.deferred wokkel-0.7.1/wokkel/subprotocols.py0000775000175000017500000003640512074262111020220 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_subprotocols -*- # # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ XMPP subprotocol support. """ __all__ = ['XMPPHandler', 'XMPPHandlerCollection', 'StreamManager', 'IQHandlerMixin'] from zope.interface import implements from twisted.internet import defer from twisted.internet.error import ConnectionDone from twisted.python import failure, log from twisted.python.deprecate import deprecatedModuleAttribute from twisted.python.versions import Version from twisted.words.protocols.jabber import error, ijabber, xmlstream from twisted.words.protocols.jabber.xmlstream import toResponse from twisted.words.protocols.jabber.xmlstream import XMPPHandlerCollection from twisted.words.xish import xpath from twisted.words.xish.domish import IElement deprecatedModuleAttribute( Version("Wokkel", 0, 7, 0), "Use twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection " "instead.", __name__, "XMPPHandlerCollection") class XMPPHandler(object): """ XMPP protocol handler. Classes derived from this class implement (part of) one or more XMPP extension protocols, and are referred to as a subprotocol implementation. """ implements(ijabber.IXMPPHandler) def __init__(self): self.parent = None self.xmlstream = None def setHandlerParent(self, parent): self.parent = parent self.parent.addHandler(self) def disownHandlerParent(self, parent): self.parent.removeHandler(self) self.parent = None def makeConnection(self, xs): self.xmlstream = xs self.connectionMade() def connectionMade(self): """ Called after a connection has been established. Can be overridden to perform work before stream initialization. """ def connectionInitialized(self): """ The XML stream has been initialized. Can be overridden to perform work after stream initialization, e.g. to set up observers and start exchanging XML stanzas. """ def connectionLost(self, reason): """ The XML stream has been closed. This method can be extended to inspect the C{reason} argument and act on it. """ self.xmlstream = None def send(self, obj): """ Send data over the managed XML stream. @note: The stream manager maintains a queue for data sent using this method when there is no current initialized XML stream. This data is then sent as soon as a new stream has been established and initialized. Subsequently, L{connectionInitialized} will be called again. If this queueing is not desired, use C{send} on C{self.xmlstream}. @param obj: data to be sent over the XML stream. This is usually an object providing L{domish.IElement}, or serialized XML. See L{xmlstream.XmlStream} for details. """ self.parent.send(obj) def request(self, request): """ Send an IQ request and track the response. This passes the request to the parent for sending and response tracking. @see: L{StreamManager.request}. """ return self.parent.request(request) class StreamManager(XMPPHandlerCollection): """ Business logic representing a managed XMPP connection. This maintains a single XMPP connection and provides facilities for packet routing and transmission. Business logic modules are objects providing L{IXMPPHandler} (like subclasses of L{XMPPHandler}), and added using L{addHandler}. @ivar xmlstream: currently managed XML stream @type xmlstream: L{XmlStream} @ivar logTraffic: if true, log all traffic. @type logTraffic: C{bool} @ivar _initialized: Whether the stream represented by L{xmlstream} has been initialized. This is used when caching outgoing stanzas. @type _initialized: C{bool} @ivar _packetQueue: internal buffer of unsent data. See L{send} for details. @type _packetQueue: L{list} @ivar timeout: Default IQ request timeout in seconds. @type timeout: C{int} @ivar _reactor: A provider of L{IReactorTime} to track timeouts. """ timeout = None _reactor = None logTraffic = False def __init__(self, factory, reactor=None): """ Construct a stream manager. @param factory: The stream factory to connect with. @param reactor: A provider of L{IReactorTime} to track timeouts. If not provided, the global reactor will be used. """ XMPPHandlerCollection.__init__(self) self.xmlstream = None self._packetQueue = [] self._initialized = False factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self._connected) factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd) factory.addBootstrap(xmlstream.INIT_FAILED_EVENT, self.initializationFailed) factory.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected) self.factory = factory if reactor is None: from twisted.internet import reactor self._reactor = reactor # Set up IQ response tracking self._iqDeferreds = {} def addHandler(self, handler): """ Add protocol handler. When an XML stream has already been established, the handler's C{connectionInitialized} will be called to get it up to speed. """ XMPPHandlerCollection.addHandler(self, handler) # get protocol handler up to speed when a connection has already # been established if self.xmlstream: handler.makeConnection(self.xmlstream) if self._initialized: handler.connectionInitialized() def _connected(self, xs): """ Called when the transport connection has been established. Here we optionally set up traffic logging (depending on L{logTraffic}) and call each handler's C{makeConnection} method with the L{XmlStream} instance. """ def logDataIn(buf): log.msg("RECV: %r" % buf) def logDataOut(buf): log.msg("SEND: %r" % buf) if self.logTraffic: xs.rawDataInFn = logDataIn xs.rawDataOutFn = logDataOut self.xmlstream = xs for e in list(self): e.makeConnection(xs) def _authd(self, xs): """ Called when the stream has been initialized. Send out cached stanzas and call each handler's C{connectionInitialized} method. """ xs.addObserver('/iq[@type="result"]', self._onIQResponse) xs.addObserver('/iq[@type="error"]', self._onIQResponse) # Flush all pending packets for p in self._packetQueue: xs.send(p) self._packetQueue = [] self._initialized = True # Notify all child services which implement # the IService interface for e in list(self): e.connectionInitialized() def initializationFailed(self, reason): """ Called when stream initialization has failed. Stream initialization has halted, with the reason indicated by C{reason}. It may be retried by calling the authenticator's C{initializeStream}. See the respective authenticators for details. @param reason: A failure instance indicating why stream initialization failed. @type reason: L{failure.Failure} """ def _disconnected(self, reason): """ Called when the stream has been closed. From this point on, the manager doesn't interact with the L{XmlStream} anymore and notifies each handler that the connection was lost by calling its C{connectionLost} method. """ self.xmlstream = None self._initialized = False # Twisted versions before 11.0 passed an XmlStream here. if not hasattr(reason, 'trap'): reason = failure.Failure(ConnectionDone()) # Notify all child services which implement # the IService interface for e in list(self): e.connectionLost(reason) # This errbacks all deferreds of iq's for which no response has # been received with a L{ConnectionLost} failure. Otherwise, the # deferreds will never be fired. iqDeferreds = self._iqDeferreds self._iqDeferreds = {} for d in iqDeferreds.itervalues(): d.errback(reason) def _onIQResponse(self, iq): """ Handle iq response by firing associated deferred. """ try: d = self._iqDeferreds[iq["id"]] except KeyError: return del self._iqDeferreds[iq["id"]] iq.handled = True if iq['type'] == 'error': d.errback(error.exceptionFromStanza(iq)) else: d.callback(iq) def send(self, obj): """ Send data over the XML stream. When there is no established XML stream, the data is queued and sent out when a new XML stream has been established and initialized. @param obj: data to be sent over the XML stream. See L{xmlstream.XmlStream.send} for details. """ if self._initialized: self.xmlstream.send(obj) else: self._packetQueue.append(obj) def request(self, request): """ Send an IQ request and track the response. A request is an IQ L{generic.Stanza} of type C{'get'} or C{'set'}. It will have its C{toElement} called to render to a L{Element} which is then sent out over the current stream. If there is no such stream (yet), it is queued and sent whenever a connection is established and initialized, just like L{send}. If the request doesn't have an identifier, it will be assigned a fresh one, so the response can be tracked. The deferred that is returned will fire with the L{Element} representation of the response if it is a result iq. If the response is an error iq, a corresponding L{error.StanzaError} will be errbacked. If the connection is closed before a response was received, the deferred will be errbacked with the reason failure. A request may also have a timeout, either by setting a default timeout in L{StreamManager}'s C{timeout} attribute or on the C{timeout} attribute of the request. @param request: The IQ request. @type request: L{generic.Request} """ if (request.stanzaKind != 'iq' or request.stanzaType not in ('get', 'set')): return defer.fail(ValueError("Not a request")) element = request.toElement() # Make sure we have a trackable id on the stanza if not request.stanzaID: element.addUniqueId() request.stanzaID = element['id'] # Set up iq response tracking d = defer.Deferred() self._iqDeferreds[element['id']] = d timeout = getattr(request, 'timeout', self.timeout) if timeout is not None: def onTimeout(): del self._iqDeferreds[element['id']] d.errback(xmlstream.TimeoutError("IQ timed out")) call = self._reactor.callLater(timeout, onTimeout) def cancelTimeout(result): if call.active(): call.cancel() return result d.addBoth(cancelTimeout) self.send(element) return d class IQHandlerMixin(object): """ XMPP subprotocol mixin for handle incoming IQ stanzas. This matches up the iq with XPath queries to call methods on itself, wrapping the call so that exceptions result in proper error responses, or, when succesful will reply with a response with optional payload. Derivatives of this class must provide an L{XmlStream} instance in its C{xmlstream} attribute. The optional payload is taken from the result of the handler and is expected to be a child or a list of childs. If an exception is raised, or the deferred has its errback called, the exception is checked for being a L{error.StanzaError}. If so, an error response is sent. Any other exception will cause a error response of C{internal-server-error} to be sent. A typical way to use this mixin, is to set up L{xpath} observers on the C{xmlstream} to call handleRequest, for example in an overridden L{XMPPHandler.connectionMade}. It is likely a good idea to only listen for incoming iq get and/org iq set requests, and not for any iq, to prevent hijacking incoming responses to outgoing iq requests. An example: >>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']" >>> class MyHandler(XMPPHandler, IQHandlerMixin): ... iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet', ... "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'} ... def connectionMade(self): ... self.xmlstream.addObserver( ... "/iq[@type='get' or @type='set']" + QUERY_ROSTER, ... self.handleRequest) ... def onRosterGet(self, iq): ... pass ... def onRosterSet(self, iq): ... pass @cvar iqHandlers: Mapping from XPath queries (as a string) to the method name that will handle requests that match the query. @type iqHandlers: C{dict} """ iqHandlers = None def handleRequest(self, iq): """ Find a handler and wrap the call for sending a response stanza. """ def toResult(result, iq): response = toResponse(iq, 'result') if result: if IElement.providedBy(result): response.addChild(result) else: for element in result: response.addChild(element) return response def checkNotImplemented(failure): failure.trap(NotImplementedError) raise error.StanzaError('feature-not-implemented') def fromStanzaError(failure, iq): failure.trap(error.StanzaError) return failure.value.toResponse(iq) def fromOtherError(failure, iq): log.msg("Unhandled error in iq handler:", isError=True) log.err(failure) return error.StanzaError('internal-server-error').toResponse(iq) handler = None for queryString, method in self.iqHandlers.iteritems(): if xpath.internQuery(queryString).matches(iq): handler = getattr(self, method) if handler: d = defer.maybeDeferred(handler, iq) else: d = defer.fail(NotImplementedError()) d.addCallback(toResult, iq) d.addErrback(checkNotImplemented) d.addErrback(fromStanzaError, iq) d.addErrback(fromOtherError, iq) d.addCallback(self.send) iq.handled = True wokkel-0.7.1/wokkel/ping.py0000775000175000017500000000567711707274572016446 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_ping -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Ping. The XMPP Ping protocol is documented in U{XEP-0199}. """ from zope.interface import implements from twisted.words.protocols.jabber.error import StanzaError from twisted.words.protocols.jabber.xmlstream import IQ, toResponse from twisted.words.protocols.jabber.xmlstream import XMPPHandler from wokkel import disco, iwokkel NS_PING = 'urn:xmpp:ping' PING_REQUEST = "/iq[@type='get']/ping[@xmlns='%s']" % NS_PING class PingClientProtocol(XMPPHandler): """ Ping client. This handler implements the protocol for sending out XMPP Ping requests. """ def ping(self, entity, sender=None): """ Send out a ping request and wait for a response. @param entity: Entity to be pinged. @type entity: L{JID} @return: A deferred that fires upon receiving a response. @rtype: L{Deferred} @param sender: Optional sender address. @type sender: L{JID} """ def cb(response): return None def eb(failure): failure.trap(StanzaError) exc = failure.value if exc.condition == 'service-unavailable': return None else: return failure request = IQ(self.xmlstream, 'get') request.addElement((NS_PING, 'ping')) if sender is not None: request['from'] = unicode(sender) d = request.send(entity.full()) d.addCallbacks(cb, eb) return d class PingHandler(XMPPHandler): """ Ping responder. This handler waits for XMPP Ping requests and sends a response. """ implements(iwokkel.IDisco) def connectionInitialized(self): """ Called when the XML stream has been initialized. This sets up an observer for incoming ping requests. """ self.xmlstream.addObserver(PING_REQUEST, self.onPing) def onPing(self, iq): """ Called when a ping request has been received. This immediately replies with a result response. """ response = toResponse(iq, 'result') self.xmlstream.send(response) iq.handled = True def getDiscoInfo(self, requestor, target, nodeIdentifier=''): """ Get identity and features from this entity, node. This handler supports XMPP Ping, but only without a nodeIdentifier specified. """ if not nodeIdentifier: return [disco.DiscoFeature(NS_PING)] else: return [] def getDiscoItems(self, requestor, target, nodeIdentifier=''): """ Get contained items for this entity, node. This handler does not support items. """ return [] wokkel-0.7.1/wokkel/compat.py0000775000175000017500000002242611707274572016763 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_compat -*- # # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Compatibility module to provide backwards compatibility with Twisted features. """ __all__ = ['BootstrapMixin', 'XmlStreamServerFactory', 'IQ', 'NamedConstant', 'ValueConstant', 'Names', 'Values'] from itertools import count from twisted.python.deprecate import deprecatedModuleAttribute from twisted.python.versions import Version from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber.xmlstream import XmlStreamServerFactory from twisted.words.xish.xmlstream import BootstrapMixin deprecatedModuleAttribute( Version("Wokkel", 0, 7, 0), "Use twisted.words.xish.xmlstream.BootstrapMixin instead.", __name__, "BootstrapMixin") deprecatedModuleAttribute( Version("Wokkel", 0, 7, 0), "Use twisted.words.protocols.jabber.xmlstream.XmlStreamServerFactory " "instead.", __name__, "XmlStreamServerFactory") class IQ(xmlstream.IQ): def __init__(self, *args, **kwargs): # Make sure we have a reactor parameter try: reactor = kwargs['reactor'] except KeyError: from twisted.internet import reactor kwargs['reactor'] = reactor # Check if IQ's init accepts the reactor parameter try: xmlstream.IQ.__init__(self, *args, **kwargs) except TypeError: # Guess not. Remove the reactor parameter and try again. del kwargs['reactor'] xmlstream.IQ.__init__(self, *args, **kwargs) # Patch the XmlStream instance so that it has a _callLater self._xmlstream._callLater = reactor.callLater _unspecified = object() _constantOrder = count().next class _Constant(object): """ @ivar _index: A C{int} allocated from a shared counter in order to keep track of the order in which L{_Constant}s are instantiated. @ivar name: A C{str} giving the name of this constant; only set once the constant is initialized by L{_ConstantsContainer}. @ivar _container: The L{_ConstantsContainer} subclass this constant belongs to; only set once the constant is initialized by that subclass. @since: Twisted 12.0.0. """ def __init__(self): self._index = _constantOrder() def __get__(self, oself, cls): """ Ensure this constant has been initialized before returning it. """ cls._initializeEnumerants() return self def __repr__(self): """ Return text identifying both which constant this is and which collection it belongs to. """ return "<%s=%s>" % (self._container.__name__, self.name) def _realize(self, container, name, value): """ Complete the initialization of this L{_Constant}. @param container: The L{_ConstantsContainer} subclass this constant is part of. @param name: The name of this constant in its container. @param value: The value of this constant; not used, as named constants have no value apart from their identity. """ self._container = container self.name = name class _EnumerantsInitializer(object): """ L{_EnumerantsInitializer} is a descriptor used to initialize a cache of objects representing named constants for a particular L{_ConstantsContainer} subclass. @since: Twisted 12.0.0. """ def __get__(self, oself, cls): """ Trigger the initialization of the enumerants cache on C{cls} and then return it. """ cls._initializeEnumerants() return cls._enumerants class _ConstantsContainer(object): """ L{_ConstantsContainer} is a class with attributes used as symbolic constants. It is up to subclasses to specify what kind of constants are allowed. @cvar _constantType: Specified by a L{_ConstantsContainer} subclass to specify the type of constants allowed by that subclass. @cvar _enumerantsInitialized: A C{bool} tracking whether C{_enumerants} has been initialized yet or not. @cvar _enumerants: A C{dict} mapping the names of constants (eg L{NamedConstant} instances) found in the class definition to those instances. This is initialized via the L{_EnumerantsInitializer} descriptor the first time it is accessed. @since: Twisted 12.0.0. """ _constantType = None _enumerantsInitialized = False _enumerants = _EnumerantsInitializer() def __new__(cls): """ Classes representing constants containers are not intended to be instantiated. The class object itself is used directly. """ raise TypeError("%s may not be instantiated." % (cls.__name__,)) def _initializeEnumerants(cls): """ Find all of the L{NamedConstant} instances in the definition of C{cls}, initialize them with constant values, and build a mapping from their names to them to attach to C{cls}. """ if not cls._enumerantsInitialized: constants = [] for (name, descriptor) in cls.__dict__.iteritems(): if isinstance(descriptor, cls._constantType): constants.append((descriptor._index, name, descriptor)) enumerants = {} for (index, enumerant, descriptor) in constants: value = cls._constantFactory(enumerant) descriptor._realize(cls, enumerant, value) enumerants[enumerant] = descriptor # Replace the _enumerants descriptor with the result so future # access will go directly to the values. The _enumerantsInitialized # flag is still necessary because NamedConstant.__get__ may also # call this method. cls._enumerants = enumerants cls._enumerantsInitialized = True _initializeEnumerants = classmethod(_initializeEnumerants) def _constantFactory(cls, name): """ Construct the value for a new constant to add to this container. @param name: The name of the constant to create. @return: L{NamedConstant} instances have no value apart from identity, so return a meaningless dummy value. """ return _unspecified _constantFactory = classmethod(_constantFactory) def lookupByName(cls, name): """ Retrieve a constant by its name or raise a C{ValueError} if there is no constant associated with that name. @param name: A C{str} giving the name of one of the constants defined by C{cls}. @raise ValueError: If C{name} is not the name of one of the constants defined by C{cls}. @return: The L{NamedConstant} associated with C{name}. """ if name in cls._enumerants: return getattr(cls, name) raise ValueError(name) lookupByName = classmethod(lookupByName) def iterconstants(cls): """ Iteration over a L{Names} subclass results in all of the constants it contains. @return: an iterator the elements of which are the L{NamedConstant} instances defined in the body of this L{Names} subclass. """ constants = cls._enumerants.values() constants.sort(key=lambda descriptor: descriptor._index) return iter(constants) iterconstants = classmethod(iterconstants) class NamedConstant(_Constant): """ L{NamedConstant} defines an attribute to be a named constant within a collection defined by a L{Names} subclass. L{NamedConstant} is only for use in the definition of L{Names} subclasses. Do not instantiate L{NamedConstant} elsewhere and do not subclass it. @since: Twisted 12.0.0. """ class Names(_ConstantsContainer): """ A L{Names} subclass contains constants which differ only in their names and identities. @since: Twisted 12.0.0. """ _constantType = NamedConstant class ValueConstant(_Constant): """ L{ValueConstant} defines an attribute to be a named constant within a collection defined by a L{Values} subclass. L{ValueConstant} is only for use in the definition of L{Values} subclasses. Do not instantiate L{ValueConstant} elsewhere and do not subclass it. @since: Twisted 12.0.0. """ def __init__(self, value): _Constant.__init__(self) self.value = value class Values(_ConstantsContainer): """ A L{Values} subclass contains constants which are associated with arbitrary values. @since: Twisted 12.0.0. """ _constantType = ValueConstant def lookupByValue(cls, value): """ Retrieve a constant by its value or raise a C{ValueError} if there is no constant associated with that value. @param value: The value of one of the constants defined by C{cls}. @raise ValueError: If C{value} is not the value of one of the constants defined by C{cls}. @return: The L{ValueConstant} associated with C{value}. """ for constant in cls.iterconstants(): if constant.value == value: return constant raise ValueError(value) lookupByValue = classmethod(lookupByValue) wokkel-0.7.1/wokkel/test/0000775000175000017500000000000012074346436016071 5ustar ralphmralphm00000000000000wokkel-0.7.1/wokkel/test/test_shim.py0000775000175000017500000000613111707215356020444 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_shim -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for {wokkel.shim}. """ from twisted.trial import unittest from wokkel import shim from wokkel.generic import parseXml NS_SHIM = 'http://jabber.org/protocol/shim' class HeadersTest(unittest.TestCase): """ Tests for L{wokkel.shim.headers}. """ def test_noHeaders(self): headers = shim.Headers([]) self.assertEquals(NS_SHIM, headers.uri) self.assertEquals('headers', headers.name) self.assertEquals([], headers.children) def test_header(self): headers = shim.Headers([('Urgency', 'high')]) elements = list(headers.elements()) self.assertEquals(1, len(elements)) header = elements[0] self.assertEquals(NS_SHIM, header.uri) self.assertEquals('header', header.name) self.assertEquals('Urgency', header['name']) self.assertEquals('high', unicode(header)) def test_headerRepeated(self): """ Some headers can appear more than once with the same name. """ headers = shim.Headers([('Collection', 'node1'), ('Collection', 'node2')]) elements = list(headers.elements()) self.assertEquals(2, len(elements)) collections = set((unicode(element) for element in elements if element['name'] == 'Collection')) self.assertIn('node1', collections) self.assertIn('node2', collections) class ExtractHeadersTest(unittest.TestCase): """ Tests for L{wokkel.shim.extractHeaders}. """ def test_noHeaders(self): """ A stanza without headers results in an empty dictionary. """ stanza = parseXml("""""") headers = shim.extractHeaders(stanza) self.assertEquals({}, headers) def test_headers(self): """ A stanza with headers results in a dictionary with those headers. """ xml = """
node1
high
""" stanza = parseXml(xml) headers = shim.extractHeaders(stanza) self.assertEquals({'Urgency': ['high'], 'Collection': ['node1']}, headers) def test_headersRepeated(self): """ Some headers may appear repeatedly. Make sure all values are extracted. """ xml = """
node1
high
node2
""" stanza = parseXml(xml) headers = shim.extractHeaders(stanza) self.assertEquals({'Urgency': ['high'], 'Collection': ['node1', 'node2']}, headers) wokkel-0.7.1/wokkel/test/test_server.py0000775000175000017500000003575511707215356021030 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.server}. """ from twisted.internet import defer from twisted.python import failure from twisted.test.proto_helpers import StringTransport from twisted.trial import unittest from twisted.words.protocols.jabber import error, jid, xmlstream from twisted.words.xish import domish from wokkel import component, server NS_STREAMS = 'http://etherx.jabber.org/streams' NS_DIALBACK = "jabber:server:dialback" class GenerateKeyTest(unittest.TestCase): """ Tests for L{server.generateKey}. """ def testBasic(self): originating = "example.org" receiving = "xmpp.example.com" sid = "D60000229F" secret = "s3cr3tf0rd14lb4ck" key = server.generateKey(secret, receiving, originating, sid) self.assertEqual(key, '37c69b1cf07a3f67c04a5ef5902fa5114f2c76fe4a2686482ba5b89323075643') class XMPPServerListenAuthenticatorTest(unittest.TestCase): """ Tests for L{server.XMPPServerListenAuthenticator}. """ secret = "s3cr3tf0rd14lb4ck" originating = "example.org" receiving = "xmpp.example.com" sid = "D60000229F" key = '37c69b1cf07a3f67c04a5ef5902fa5114f2c76fe4a2686482ba5b89323075643' def setUp(self): self.output = [] class MyService(object): pass self.service = MyService() self.service.defaultDomain = self.receiving self.service.domains = [self.receiving, 'pubsub.'+self.receiving] self.service.secret = self.secret self.authenticator = server.XMPPServerListenAuthenticator(self.service) self.xmlstream = xmlstream.XmlStream(self.authenticator) self.xmlstream.send = self.output.append self.xmlstream.transport = StringTransport() def test_attributes(self): """ Test attributes of authenticator and stream objects. """ self.assertEqual(self.service, self.authenticator.service) self.assertEqual(self.xmlstream.initiating, False) def test_streamStartedVersion0(self): """ The authenticator supports pre-XMPP 1.0 streams. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual((0, 0), self.xmlstream.version) def test_streamStartedVersion1(self): """ The authenticator supports XMPP 1.0 streams. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual((1, 0), self.xmlstream.version) def test_streamStartedSID(self): """ The response stream will have a stream ID. """ self.xmlstream.connectionMade() self.assertIdentical(None, self.xmlstream.sid) self.xmlstream.dataReceived( "") self.assertNotIdentical(None, self.xmlstream.sid) def test_streamStartedSentResponseHeader(self): """ A stream header is sent in response to the incoming stream header. """ self.xmlstream.connectionMade() self.assertFalse(self.xmlstream._headerSent) self.xmlstream.dataReceived( "") self.assertTrue(self.xmlstream._headerSent) def test_streamStartedNotSentFeatures(self): """ No features are sent in response to an XMPP < 1.0 stream header. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(1, len(self.output)) def test_streamStartedSentFeatures(self): """ Features are sent in response to an XMPP >= 1.0 stream header. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(2, len(self.output)) features = self.output[-1] self.assertEqual(NS_STREAMS, features.uri) self.assertEqual('features', features.name) def test_streamRootElement(self): """ Test stream error on wrong stream namespace. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(3, len(self.output)) exc = error.exceptionFromStreamError(self.output[1]) self.assertEqual('invalid-namespace', exc.condition) def test_streamDefaultNamespace(self): """ Test stream error on missing dialback namespace. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(3, len(self.output)) exc = error.exceptionFromStreamError(self.output[1]) self.assertEqual('invalid-namespace', exc.condition) def test_streamNoDialbackNamespace(self): """ Test stream error on missing dialback namespace. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(3, len(self.output)) exc = error.exceptionFromStreamError(self.output[1]) self.assertEqual('invalid-namespace', exc.condition) def test_streamBadDialbackNamespace(self): """ Test stream error on missing dialback namespace. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(3, len(self.output)) exc = error.exceptionFromStreamError(self.output[1]) self.assertEqual('invalid-namespace', exc.condition) def test_streamToUnknownHost(self): """ Test stream error on stream's to attribute having unknown host. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(3, len(self.output)) exc = error.exceptionFromStreamError(self.output[1]) self.assertEqual('host-unknown', exc.condition) def test_streamToOtherLocalHost(self): """ The authenticator supports XMPP 1.0 streams. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.assertEqual(2, len(self.output)) self.assertEqual(jid.JID('pubsub.xmpp.example.com'), self.xmlstream.thisEntity) def test_onResult(self): def cb(result): self.assertEqual(1, len(self.output)) reply = self.output[0] self.assertEqual(self.originating, reply['to']) self.assertEqual(self.receiving, reply['from']) self.assertEqual('valid', reply['type']) def validateConnection(thisHost, otherHost, sid, key): self.assertEqual(thisHost, self.receiving) self.assertEqual(otherHost, self.originating) self.assertEqual(sid, self.sid) self.assertEqual(key, self.key) return defer.succeed(None) self.xmlstream.sid = self.sid self.service.validateConnection = validateConnection result = domish.Element((NS_DIALBACK, 'result')) result['to'] = self.receiving result['from'] = self.originating result.addContent(self.key) d = self.authenticator.onResult(result) d.addCallback(cb) return d def test_onResultFailure(self): class TestError(Exception): pass def cb(result): reply = self.output[0] self.assertEqual('invalid', reply['type']) self.assertEqual(1, len(self.flushLoggedErrors(TestError))) def validateConnection(thisHost, otherHost, sid, key): return defer.fail(TestError()) self.xmlstream.sid = self.sid self.service.validateConnection = validateConnection result = domish.Element((NS_DIALBACK, 'result')) result['to'] = self.receiving result['from'] = self.originating result.addContent(self.key) d = self.authenticator.onResult(result) d.addCallback(cb) return d class FakeService(object): domains = set(['example.org', 'pubsub.example.org']) defaultDomain = 'example.org' secret = 'mysecret' def __init__(self): self.dispatched = [] def dispatch(self, xs, element): self.dispatched.append(element) class XMPPS2SServerFactoryTest(unittest.TestCase): """ Tests for L{component.XMPPS2SServerFactory}. """ def setUp(self): self.service = FakeService() self.factory = server.XMPPS2SServerFactory(self.service) self.xmlstream = self.factory.buildProtocol(None) self.transport = StringTransport() self.xmlstream.thisEntity = jid.JID('example.org') self.xmlstream.otherEntity = jid.JID('example.com') def test_makeConnection(self): """ A new connection increases the stream serial count. No logs by default. """ self.xmlstream.makeConnection(self.transport) self.assertEqual(0, self.xmlstream.serial) self.assertEqual(1, self.factory.serial) self.assertIdentical(None, self.xmlstream.rawDataInFn) self.assertIdentical(None, self.xmlstream.rawDataOutFn) def test_makeConnectionLogTraffic(self): """ Setting logTraffic should set up raw data loggers. """ self.factory.logTraffic = True self.xmlstream.makeConnection(self.transport) self.assertNotIdentical(None, self.xmlstream.rawDataInFn) self.assertNotIdentical(None, self.xmlstream.rawDataOutFn) def test_onError(self): """ An observer for stream errors should trigger onError to log it. """ self.xmlstream.makeConnection(self.transport) class TestError(Exception): pass reason = failure.Failure(TestError()) self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT) self.assertEqual(1, len(self.flushLoggedErrors(TestError))) def test_connectionInitialized(self): """ """ self.xmlstream.makeConnection(self.transport) self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) def test_connectionLost(self): """ """ self.xmlstream.makeConnection(self.transport) self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT) def test_Element(self): self.xmlstream.makeConnection(self.transport) self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) stanza = domish.Element((None, "presence")) self.xmlstream.dispatch(stanza) self.assertEqual(1, len(self.service.dispatched)) self.assertIdentical(stanza, self.service.dispatched[-1]) def test_ElementNotAuthenticated(self): self.xmlstream.makeConnection(self.transport) stanza = domish.Element((None, "presence")) self.xmlstream.dispatch(stanza) self.assertEqual(0, len(self.service.dispatched)) class ServerServiceTest(unittest.TestCase): def setUp(self): self.output = [] self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator()) self.xmlstream.thisEntity = jid.JID('example.org') self.xmlstream.otherEntity = jid.JID('example.com') self.xmlstream.send = self.output.append self.router = component.Router() self.service = server.ServerService(self.router, secret='mysecret', domain='example.org') self.service.xmlstream = self.xmlstream def test_defaultDomainInDomains(self): """ The default domain is part of the domains considered local. """ self.assertIn(self.service.defaultDomain, self.service.domains) def test_dispatch(self): stanza = domish.Element((None, "presence")) stanza['to'] = 'user@example.org' stanza['from'] = 'other@example.com' self.service.dispatch(self.xmlstream, stanza) self.assertEqual(1, len(self.output)) self.assertIdentical(stanza, self.output[-1]) def test_dispatchNoTo(self): errors = [] self.xmlstream.sendStreamError = errors.append stanza = domish.Element((None, "presence")) stanza['from'] = 'other@example.com' self.service.dispatch(self.xmlstream, stanza) self.assertEqual(1, len(errors)) wokkel-0.7.1/wokkel/test/test_compat.py0000775000175000017500000003731711707274572021006 0ustar ralphmralphm00000000000000# Copyright (c) Twisted Matrix Laboratories. # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.compat}. """ from zope.interface import implements from twisted.internet import task from twisted.internet.interfaces import IReactorTime from twisted.trial import unittest from twisted.words.protocols.jabber import xmlstream from wokkel.compat import IQ from wokkel.compat import NamedConstant, Names, ValueConstant, Values class DeprecationTest(unittest.TestCase): """ Deprecation tests for L{wokkel.compat}. """ def lookForDeprecationWarning(self, testmethod, attributeName, newName): """ Importing C{testmethod} emits a deprecation warning. """ warningsShown = self.flushWarnings([testmethod]) self.assertEqual(len(warningsShown), 1) self.assertIdentical(warningsShown[0]['category'], DeprecationWarning) self.assertEqual( warningsShown[0]['message'], "wokkel.compat." + attributeName + " " "was deprecated in Wokkel 0.7.0: Use " + newName + " instead.") def test_bootstrapMixinTest(self): """ L{compat.BootstrapMixin} is deprecated. """ from wokkel.compat import BootstrapMixin BootstrapMixin self.lookForDeprecationWarning( self.test_bootstrapMixinTest, "BootstrapMixin", "twisted.words.xish.xmlstream.BootstrapMixin") def test_xmlStreamServerFactory(self): """ L{compat.XmlStreamServerFactory} is deprecated. """ from wokkel.compat import XmlStreamServerFactory XmlStreamServerFactory self.lookForDeprecationWarning( self.test_xmlStreamServerFactory, "XmlStreamServerFactory", "twisted.words.protocols.jabber.xmlstream." "XmlStreamServerFactory") class FakeReactor(object): implements(IReactorTime) def __init__(self): self.clock = task.Clock() self.callLater = self.clock.callLater self.getDelayedCalls = self.clock.getDelayedCalls class IQTest(unittest.TestCase): """ Tests for L{IQ}. """ def setUp(self): self.reactor = FakeReactor() self.clock = self.reactor.clock def testRequestTimingOutEventDispatcher(self): """ Test that an iq request with a defined timeout times out. """ from twisted.words.xish import utility output = [] xs = utility.EventDispatcher() xs.send = output.append self.iq = IQ(xs, reactor=self.reactor) self.iq.timeout = 60 d = self.iq.send() self.assertFailure(d, xmlstream.TimeoutError) self.clock.pump([1, 60]) self.assertFalse(self.reactor.getDelayedCalls()) self.assertFalse(xs.iqDeferreds) return d class NamedConstantTests(unittest.TestCase): """ Tests for the L{twisted.python.constants.NamedConstant} class which is used to represent individual values. """ def setUp(self): """ Create a dummy container into which constants can be placed. """ class foo(Names): pass self.container = foo def test_name(self): """ The C{name} attribute of a L{NamedConstant} refers to the value passed for the C{name} parameter to C{_realize}. """ name = NamedConstant() name._realize(self.container, "bar", None) self.assertEqual("bar", name.name) def test_representation(self): """ The string representation of an instance of L{NamedConstant} includes the container the instances belongs to as well as the instance's name. """ name = NamedConstant() name._realize(self.container, "bar", None) self.assertEqual("", repr(name)) def test_equality(self): """ A L{NamedConstant} instance compares equal to itself. """ name = NamedConstant() name._realize(self.container, "bar", None) self.assertTrue(name == name) self.assertFalse(name != name) def test_nonequality(self): """ Two different L{NamedConstant} instances do not compare equal to each other. """ first = NamedConstant() first._realize(self.container, "bar", None) second = NamedConstant() second._realize(self.container, "bar", None) self.assertFalse(first == second) self.assertTrue(first != second) def test_hash(self): """ Because two different L{NamedConstant} instances do not compare as equal to each other, they also have different hashes to avoid collisions when added to a C{dict} or C{set}. """ first = NamedConstant() first._realize(self.container, "bar", None) second = NamedConstant() second._realize(self.container, "bar", None) self.assertNotEqual(hash(first), hash(second)) class _ConstantsTestsMixin(object): """ Mixin defining test helpers common to multiple types of constants collections. """ def _notInstantiableTest(self, name, cls): """ Assert that an attempt to instantiate the constants class raises C{TypeError}. @param name: A C{str} giving the name of the constants collection. @param cls: The constants class to test. """ exc = self.assertRaises(TypeError, cls) self.assertEqual(name + " may not be instantiated.", str(exc)) class NamesTests(unittest.TestCase, _ConstantsTestsMixin): """ Tests for L{twisted.python.constants.Names}, a base class for containers of related constaints. """ def setUp(self): """ Create a fresh new L{Names} subclass for each unit test to use. Since L{Names} is stateful, re-using the same subclass across test methods makes exercising all of the implementation code paths difficult. """ class METHOD(Names): """ A container for some named constants to use in unit tests for L{Names}. """ GET = NamedConstant() PUT = NamedConstant() POST = NamedConstant() DELETE = NamedConstant() self.METHOD = METHOD def test_notInstantiable(self): """ A subclass of L{Names} raises C{TypeError} if an attempt is made to instantiate it. """ self._notInstantiableTest("METHOD", self.METHOD) def test_symbolicAttributes(self): """ Each name associated with a L{NamedConstant} instance in the definition of a L{Names} subclass is available as an attribute on the resulting class. """ self.assertTrue(hasattr(self.METHOD, "GET")) self.assertTrue(hasattr(self.METHOD, "PUT")) self.assertTrue(hasattr(self.METHOD, "POST")) self.assertTrue(hasattr(self.METHOD, "DELETE")) def test_withoutOtherAttributes(self): """ As usual, names not defined in the class scope of a L{Names} subclass are not available as attributes on the resulting class. """ self.assertFalse(hasattr(self.METHOD, "foo")) def test_representation(self): """ The string representation of a constant on a L{Names} subclass includes the name of the L{Names} subclass and the name of the constant itself. """ self.assertEqual("", repr(self.METHOD.GET)) def test_lookupByName(self): """ Constants can be looked up by name using L{Names.lookupByName}. """ method = self.METHOD.lookupByName("GET") self.assertIdentical(self.METHOD.GET, method) def test_notLookupMissingByName(self): """ Names not defined with a L{NamedConstant} instance cannot be looked up using L{Names.lookupByName}. """ self.assertRaises(ValueError, self.METHOD.lookupByName, "lookupByName") self.assertRaises(ValueError, self.METHOD.lookupByName, "__init__") self.assertRaises(ValueError, self.METHOD.lookupByName, "foo") def test_name(self): """ The C{name} attribute of one of the named constants gives that constant's name. """ self.assertEqual("GET", self.METHOD.GET.name) def test_attributeIdentity(self): """ Repeated access of an attribute associated with a L{NamedConstant} value in a L{Names} subclass results in the same object. """ self.assertIdentical(self.METHOD.GET, self.METHOD.GET) def test_iterconstants(self): """ L{Names.iterconstants} returns an iterator over all of the constants defined in the class, in the order they were defined. """ constants = list(self.METHOD.iterconstants()) self.assertEqual( [self.METHOD.GET, self.METHOD.PUT, self.METHOD.POST, self.METHOD.DELETE], constants) def test_attributeIterconstantsIdentity(self): """ The constants returned from L{Names.iterconstants} are identical to the constants accessible using attributes. """ constants = list(self.METHOD.iterconstants()) self.assertIdentical(self.METHOD.GET, constants[0]) self.assertIdentical(self.METHOD.PUT, constants[1]) self.assertIdentical(self.METHOD.POST, constants[2]) self.assertIdentical(self.METHOD.DELETE, constants[3]) def test_iterconstantsIdentity(self): """ The constants returned from L{Names.iterconstants} are identical on each call to that method. """ constants = list(self.METHOD.iterconstants()) again = list(self.METHOD.iterconstants()) self.assertIdentical(again[0], constants[0]) self.assertIdentical(again[1], constants[1]) self.assertIdentical(again[2], constants[2]) self.assertIdentical(again[3], constants[3]) def test_initializedOnce(self): """ L{Names._enumerants} is initialized once and its value re-used on subsequent access. """ first = self.METHOD._enumerants self.METHOD.GET # Side-effects! second = self.METHOD._enumerants self.assertIdentical(first, second) class ValuesTests(unittest.TestCase, _ConstantsTestsMixin): """ Tests for L{twisted.python.constants.Names}, a base class for containers of related constaints with arbitrary values. """ def setUp(self): """ Create a fresh new L{Values} subclass for each unit test to use. Since L{Values} is stateful, re-using the same subclass across test methods makes exercising all of the implementation code paths difficult. """ class STATUS(Values): OK = ValueConstant("200") NOT_FOUND = ValueConstant("404") self.STATUS = STATUS def test_notInstantiable(self): """ A subclass of L{Values} raises C{TypeError} if an attempt is made to instantiate it. """ self._notInstantiableTest("STATUS", self.STATUS) def test_symbolicAttributes(self): """ Each name associated with a L{ValueConstant} instance in the definition of a L{Values} subclass is available as an attribute on the resulting class. """ self.assertTrue(hasattr(self.STATUS, "OK")) self.assertTrue(hasattr(self.STATUS, "NOT_FOUND")) def test_withoutOtherAttributes(self): """ As usual, names not defined in the class scope of a L{Values} subclass are not available as attributes on the resulting class. """ self.assertFalse(hasattr(self.STATUS, "foo")) def test_representation(self): """ The string representation of a constant on a L{Values} subclass includes the name of the L{Values} subclass and the name of the constant itself. """ self.assertEqual("", repr(self.STATUS.OK)) def test_lookupByName(self): """ Constants can be looked up by name using L{Values.lookupByName}. """ method = self.STATUS.lookupByName("OK") self.assertIdentical(self.STATUS.OK, method) def test_notLookupMissingByName(self): """ Names not defined with a L{ValueConstant} instance cannot be looked up using L{Values.lookupByName}. """ self.assertRaises(ValueError, self.STATUS.lookupByName, "lookupByName") self.assertRaises(ValueError, self.STATUS.lookupByName, "__init__") self.assertRaises(ValueError, self.STATUS.lookupByName, "foo") def test_lookupByValue(self): """ Constants can be looked up by their associated value, defined by the argument passed to L{ValueConstant}, using L{Values.lookupByValue}. """ status = self.STATUS.lookupByValue("200") self.assertIdentical(self.STATUS.OK, status) def test_lookupDuplicateByValue(self): """ If more than one constant is associated with a particular value, L{Values.lookupByValue} returns whichever of them is defined first. """ class TRANSPORT_MESSAGE(Values): """ Message types supported by an SSH transport. """ KEX_DH_GEX_REQUEST_OLD = ValueConstant(30) KEXDH_INIT = ValueConstant(30) self.assertIdentical( TRANSPORT_MESSAGE.lookupByValue(30), TRANSPORT_MESSAGE.KEX_DH_GEX_REQUEST_OLD) def test_notLookupMissingByValue(self): """ L{Values.lookupByValue} raises L{ValueError} when called with a value with which no constant is associated. """ self.assertRaises(ValueError, self.STATUS.lookupByValue, "OK") self.assertRaises(ValueError, self.STATUS.lookupByValue, 200) self.assertRaises(ValueError, self.STATUS.lookupByValue, "200.1") def test_name(self): """ The C{name} attribute of one of the constants gives that constant's name. """ self.assertEqual("OK", self.STATUS.OK.name) def test_attributeIdentity(self): """ Repeated access of an attribute associated with a L{ValueConstant} value in a L{Values} subclass results in the same object. """ self.assertIdentical(self.STATUS.OK, self.STATUS.OK) def test_iterconstants(self): """ L{Values.iterconstants} returns an iterator over all of the constants defined in the class, in the order they were defined. """ constants = list(self.STATUS.iterconstants()) self.assertEqual( [self.STATUS.OK, self.STATUS.NOT_FOUND], constants) def test_attributeIterconstantsIdentity(self): """ The constants returned from L{Values.iterconstants} are identical to the constants accessible using attributes. """ constants = list(self.STATUS.iterconstants()) self.assertIdentical(self.STATUS.OK, constants[0]) self.assertIdentical(self.STATUS.NOT_FOUND, constants[1]) def test_iterconstantsIdentity(self): """ The constants returned from L{Values.iterconstants} are identical on each call to that method. """ constants = list(self.STATUS.iterconstants()) again = list(self.STATUS.iterconstants()) self.assertIdentical(again[0], constants[0]) self.assertIdentical(again[1], constants[1]) def test_initializedOnce(self): """ L{Values._enumerants} is initialized once and its value re-used on subsequent access. """ first = self.STATUS._enumerants self.STATUS.OK # Side-effects! second = self.STATUS._enumerants self.assertIdentical(first, second) wokkel-0.7.1/wokkel/test/helpers.py0000775000175000017500000000717012074262111020100 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Unit test helpers. """ from twisted.internet import defer from twisted.words.xish import xpath from twisted.words.xish.utility import EventDispatcher from wokkel.generic import parseXml from wokkel.subprotocols import StreamManager class XmlStreamStub(object): """ Stub for testing objects that communicate through XML Streams. Instances of this stub hold an object in L{xmlstream} that acts like an L{XmlStream} as its first argument. These appear in sequence in the L{output} instance variable of the stub. For the reverse direction, stanzas passed to L{send} of the stub, will be dispatched in the stubbed XmlStream as if it was received over the wire, so that registered observers will be called. Example: >>> stub = XmlStreamStub() >>> stub.xmlstream.send(domish.Element((None, 'presence'))) >>> stub.output[-1].toXml() u'' >>> def cb(stanza): ... print "Got: %r" stanza.toXml() >>> stub.xmlstream.addObserver('/presence') >>> stub.send(domish.Element((None, 'presence'))) Got: u'' @ivar xmlstream: Stubbed XML Stream. @type xmlstream: L{EventDispatcher} @ivar output: List of stanzas sent to the XML Stream. @type output: L{list} """ def __init__(self): self.output = [] self.xmlstream = EventDispatcher() self.xmlstream.send = self.output.append def send(self, obj): """ Pass an element to the XML Stream as if received. @param obj: Element to be dispatched to C{self.xmlstream}. @type obj: object implementing L{IElement}. """ self.xmlstream.dispatch(obj) class TestableRequestHandlerMixin(object): """ Mixin for testing XMPPHandlers that process iq requests. Handlers that use L{wokkel.subprotocols.IQHandlerMixin} define a C{iqHandlers} attribute that lists the handlers to be called for iq requests. This mixin provides L{handleRequest} to mimic the handler processing for easier testing. """ def handleRequest(self, xml): """ Find a handler and call it directly. @param xml: XML stanza that may yield a handler being called. @type xml: C{str}. @return: Deferred that fires with the result of a handler for this stanza. If no handler was found, the deferred has its errback called with a C{NotImplementedError} exception. """ handler = None iq = parseXml(xml) for queryString, method in self.service.iqHandlers.iteritems(): if xpath.internQuery(queryString).matches(iq): handler = getattr(self.service, method) if handler: d = defer.maybeDeferred(handler, iq) else: d = defer.fail(NotImplementedError()) return d class TestableStreamManager(StreamManager): """ Stream manager for testing subprotocol handlers. """ def __init__(self, reactor=None): class DummyFactory(object): def addBootstrap(self, event, fn): pass factory = DummyFactory() StreamManager.__init__(self, factory, reactor) self.stub = XmlStreamStub() self._connected(self.stub.xmlstream) self._authd(self.stub.xmlstream) wokkel-0.7.1/wokkel/test/test_component.py0000775000175000017500000004076411722233042021505 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.component}. """ from zope.interface.verify import verifyObject from twisted.internet.base import BaseConnector from twisted.internet.error import ConnectionRefusedError from twisted.internet.task import Clock from twisted.python import failure from twisted.trial import unittest from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber.ijabber import IXMPPHandlerCollection from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.words.xish import domish from wokkel import component from wokkel.generic import XmlPipe class FakeConnector(BaseConnector): """ Fake connector that counts connection attempts. """ connects = 0 def connect(self): self.connects += 1 BaseConnector.connect(self) def _makeTransport(self): return None class TestableComponent(component.Component): """ Testable component. This component provides the created factory with a L{Clock} instead of the regular reactor and uses L{FakeConnector} for testing connects and reconnects. """ def __init__(self, *args, **kwargs): component.Component.__init__(self, *args, **kwargs) self.factory.clock = Clock() def _getConnection(self): c = FakeConnector(self.factory, None, None) c.connect() return c class ComponentTest(unittest.TestCase): """ Tests for L{component.Component}. """ def test_startServiceReconnectAfterFailure(self): """ When the first connection attempt fails, retry. """ comp = TestableComponent('example.org', 5347, 'test.example.org', 'secret') # Starting the service initiates a connection attempt. comp.startService() connector = comp._connection self.assertEqual(1, connector.connects) # Fail the connection. connector.connectionFailed(ConnectionRefusedError()) # After a back-off delay, a new connection is attempted. comp.factory.clock.advance(5) self.assertEqual(2, connector.connects) def test_stopServiceNoReconnect(self): """ When the service is stopped, no reconnect is attempted. """ comp = TestableComponent('example.org', 5347, 'test.example.org', 'secret') # Starting the service initiates a connection attempt. comp.startService() connector = comp._connection # Fail the connection. connector.connectionFailed(ConnectionRefusedError()) # If the service is stopped before the back-off delay expires, # no new connection is attempted. comp.factory.clock.advance(1) comp.stopService() comp.factory.clock.advance(4) self.assertEqual(1, connector.connects) class InternalComponentTest(unittest.TestCase): """ Tests for L{component.InternalComponent}. """ def setUp(self): self.router = component.Router() self.component = component.InternalComponent(self.router, 'component') def test_interface(self): """ L{component.InternalComponent} implements L{IXMPPHandlerCollection}. """ verifyObject(IXMPPHandlerCollection, self.component) def test_startServiceRunning(self): """ Starting the service makes it running. """ self.assertFalse(self.component.running) self.component.startService() self.assertTrue(self.component.running) def test_startServiceAddRoute(self): """ Starting the service creates a new route. """ self.component.startService() self.assertIn('component', self.router.routes) def test_startServiceNoDomain(self): self.component = component.InternalComponent(self.router) self.component.startService() def test_startServiceAddMultipleRoutes(self): """ Starting the service creates a new route. """ self.component.domains.add('component2') self.component.startService() self.assertIn('component', self.router.routes) self.assertIn('component2', self.router.routes) def test_startServiceHandlerDispatch(self): """ Starting the service hooks up handlers. """ events = [] class TestHandler(XMPPHandler): def connectionInitialized(self): fn = lambda obj: events.append(obj) self.xmlstream.addObserver('//event/test', fn) TestHandler().setHandlerParent(self.component) self.component.startService() self.assertEquals([], events) self.component.xmlstream.dispatch(None, '//event/test') self.assertEquals([None], events) def test_stopServiceNotRunning(self): """ Stopping the service makes it not running. """ self.component.startService() self.component.stopService() self.assertFalse(self.component.running) def test_stopServiceRemoveRoute(self): """ Stopping the service removes routes. """ self.component.startService() self.component.stopService() self.assertNotIn('component', self.router.routes) def test_stopServiceNoDomain(self): self.component = component.InternalComponent(self.router) self.component.startService() self.component.stopService() def test_startServiceRemoveMultipleRoutes(self): """ Starting the service creates a new route. """ self.component.domains.add('component2') self.component.startService() self.component.stopService() self.assertNotIn('component', self.router.routes) self.assertNotIn('component2', self.router.routes) def test_stopServiceHandlerDispatch(self): """ Stopping the service disconnects handlers. """ events = [] class TestHandler(XMPPHandler): def connectionLost(self, reason): events.append(reason) TestHandler().setHandlerParent(self.component) self.component.startService() self.component.stopService() self.assertEquals(1, len(events)) def test_addHandler(self): """ Adding a handler connects it to the stream. """ events = [] class TestHandler(XMPPHandler): def connectionInitialized(self): fn = lambda obj: events.append(obj) self.xmlstream.addObserver('//event/test', fn) self.component.startService() self.component.xmlstream.dispatch(None, '//event/test') self.assertEquals([], events) TestHandler().setHandlerParent(self.component) self.component.xmlstream.dispatch(None, '//event/test') self.assertEquals([None], events) def test_send(self): """ A message sent from the component ends up at the router. """ events = [] fn = lambda obj: events.append(obj) message = domish.Element((None, 'message')) self.router.route = fn self.component.startService() self.component.send(message) self.assertEquals([message], events) class RouterTest(unittest.TestCase): """ Tests for L{component.Router}. """ def test_addRoute(self): """ Test route registration and routing on incoming stanzas. """ router = component.Router() routed = [] router.route = lambda element: routed.append(element) pipe = XmlPipe() router.addRoute('example.org', pipe.sink) self.assertEquals(1, len(router.routes)) self.assertEquals(pipe.sink, router.routes['example.org']) element = domish.Element(('testns', 'test')) pipe.source.send(element) self.assertEquals([element], routed) def test_route(self): """ Test routing of a message. """ component1 = XmlPipe() component2 = XmlPipe() router = component.Router() router.addRoute('component1.example.org', component1.sink) router.addRoute('component2.example.org', component2.sink) outgoing = [] component2.source.addObserver('/*', lambda element: outgoing.append(element)) stanza = domish.Element((None, 'presence')) stanza['from'] = 'component1.example.org' stanza['to'] = 'component2.example.org' component1.source.send(stanza) self.assertEquals([stanza], outgoing) def test_routeDefault(self): """ Test routing of a message using the default route. The default route is the one with C{None} as its key in the routing table. It is taken when there is no more specific route in the routing table that matches the stanza's destination. """ component1 = XmlPipe() s2s = XmlPipe() router = component.Router() router.addRoute('component1.example.org', component1.sink) router.addRoute(None, s2s.sink) outgoing = [] s2s.source.addObserver('/*', lambda element: outgoing.append(element)) stanza = domish.Element((None, 'presence')) stanza['from'] = 'component1.example.org' stanza['to'] = 'example.com' component1.source.send(stanza) self.assertEquals([stanza], outgoing) class ListenComponentAuthenticatorTest(unittest.TestCase): """ Tests for L{component.ListenComponentAuthenticator}. """ def setUp(self): self.output = [] authenticator = component.ListenComponentAuthenticator('secret') self.xmlstream = xmlstream.XmlStream(authenticator) self.xmlstream.send = self.output.append def loseConnection(self): """ Stub loseConnection because we are a transport. """ self.xmlstream.connectionLost("no reason") def test_streamStarted(self): """ The received stream header should set several attributes. """ observers = [] def addOnetimeObserver(event, observerfn): observers.append((event, observerfn)) xs = self.xmlstream xs.addOnetimeObserver = addOnetimeObserver xs.makeConnection(self) self.assertIdentical(None, xs.sid) self.assertFalse(xs._headerSent) xs.dataReceived("") self.assertEqual((0, 0), xs.version) self.assertNotIdentical(None, xs.sid) self.assertTrue(xs._headerSent) self.assertEquals(('/*', xs.authenticator.onElement), observers[-1]) def test_streamStartedWrongNamespace(self): """ The received stream header should have a correct namespace. """ streamErrors = [] xs = self.xmlstream xs.sendStreamError = streamErrors.append xs.makeConnection(self) xs.dataReceived("") self.assertEquals(1, len(streamErrors)) self.assertEquals('invalid-namespace', streamErrors[-1].condition) def test_streamStartedNoTo(self): """ The received stream header should have a 'to' attribute. """ streamErrors = [] xs = self.xmlstream xs.sendStreamError = streamErrors.append xs.makeConnection(self) xs.dataReceived("") self.assertEquals(1, len(streamErrors)) self.assertEquals('improper-addressing', streamErrors[-1].condition) def test_onElement(self): """ We expect a handshake element with a hash. """ handshakes = [] xs = self.xmlstream xs.authenticator.onHandshake = handshakes.append handshake = domish.Element(('jabber:component:accept', 'handshake')) handshake.addContent('1234') xs.authenticator.onElement(handshake) self.assertEqual('1234', handshakes[-1]) def test_onElementNotHandshake(self): """ Reject elements that are not handshakes """ handshakes = [] streamErrors = [] xs = self.xmlstream xs.authenticator.onHandshake = handshakes.append xs.sendStreamError = streamErrors.append element = domish.Element(('jabber:component:accept', 'message')) xs.authenticator.onElement(element) self.assertFalse(handshakes) self.assertEquals('not-authorized', streamErrors[-1].condition) def test_onHandshake(self): """ Receiving a handshake matching the secret authenticates the stream. """ authd = [] def authenticated(xs): authd.append(xs) xs = self.xmlstream xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated) xs.sid = u'1234' theHash = '32532c0f7dbf1253c095b18b18e36d38d94c1256' xs.authenticator.onHandshake(theHash) self.assertEqual('', self.output[-1]) self.assertEquals(1, len(authd)) def test_onHandshakeWrongHash(self): """ Receiving a bad handshake should yield a stream error. """ streamErrors = [] authd = [] def authenticated(xs): authd.append(xs) xs = self.xmlstream xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated) xs.sendStreamError = streamErrors.append xs.sid = u'1234' theHash = '1234' xs.authenticator.onHandshake(theHash) self.assertEquals('not-authorized', streamErrors[-1].condition) self.assertEquals(0, len(authd)) class XMPPComponentServerFactoryTest(unittest.TestCase): """ Tests for L{component.XMPPComponentServerFactory}. """ def setUp(self): self.router = component.Router() self.factory = component.XMPPComponentServerFactory(self.router, 'secret') self.xmlstream = self.factory.buildProtocol(None) self.xmlstream.thisEntity = JID('component.example.org') def test_makeConnection(self): """ A new connection increases the stream serial count. No logs by default. """ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT) self.assertEqual(0, self.xmlstream.serial) self.assertEqual(1, self.factory.serial) self.assertIdentical(None, self.xmlstream.rawDataInFn) self.assertIdentical(None, self.xmlstream.rawDataOutFn) def test_makeConnectionLogTraffic(self): """ Setting logTraffic should set up raw data loggers. """ self.factory.logTraffic = True self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT) self.assertNotIdentical(None, self.xmlstream.rawDataInFn) self.assertNotIdentical(None, self.xmlstream.rawDataOutFn) def test_onError(self): """ An observer for stream errors should trigger onError to log it. """ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_CONNECTED_EVENT) class TestError(Exception): pass reason = failure.Failure(TestError()) self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT) self.assertEqual(1, len(self.flushLoggedErrors(TestError))) def test_connectionInitialized(self): """ Make sure a new stream is added to the routing table. """ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) self.assertIn('component.example.org', self.router.routes) self.assertIdentical(self.xmlstream, self.router.routes['component.example.org']) def test_connectionLost(self): """ Make sure a stream is removed from the routing table on disconnect. """ self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT) self.assertNotIn('component.example.org', self.router.routes) wokkel-0.7.1/wokkel/test/test_xmppim.py0000664000175000017500000012365012074337005021013 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details """ Tests for L{wokkel.xmppim}. """ from twisted.internet import defer from twisted.trial import unittest from twisted.words.protocols.jabber import error from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import toResponse from twisted.words.xish import domish, utility from wokkel import xmppim from wokkel.generic import ErrorStanza, parseXml from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub NS_XML = 'http://www.w3.org/XML/1998/namespace' NS_ROSTER = 'jabber:iq:roster' class PresenceClientProtocolTest(unittest.TestCase): def setUp(self): self.output = [] self.protocol = xmppim.PresenceClientProtocol() self.protocol.parent = self def send(self, obj): self.output.append(obj) def test_unavailableDirected(self): """ Test sending of directed unavailable presence broadcast. """ self.protocol.unavailable(JID('user@example.com')) presence = self.output[-1] self.assertEquals("presence", presence.name) self.assertEquals(None, presence.uri) self.assertEquals("user@example.com", presence.getAttribute('to')) self.assertEquals("unavailable", presence.getAttribute('type')) def test_unavailableWithStatus(self): """ Test sending of directed unavailable presence broadcast with status. """ self.protocol.unavailable(JID('user@example.com'), {None: 'Disconnected'}) presence = self.output[-1] self.assertEquals("presence", presence.name) self.assertEquals(None, presence.uri) self.assertEquals("user@example.com", presence.getAttribute('to')) self.assertEquals("unavailable", presence.getAttribute('type')) self.assertEquals("Disconnected", unicode(presence.status)) def test_unavailableBroadcast(self): """ Test sending of unavailable presence broadcast. """ self.protocol.unavailable(None) presence = self.output[-1] self.assertEquals("presence", presence.name) self.assertEquals(None, presence.uri) self.assertEquals(None, presence.getAttribute('to')) self.assertEquals("unavailable", presence.getAttribute('type')) def test_unavailableBroadcastNoEntityParameter(self): """ Test sending of unavailable presence broadcast by not passing entity. """ self.protocol.unavailable() presence = self.output[-1] self.assertEquals("presence", presence.name) self.assertEquals(None, presence.uri) self.assertEquals(None, presence.getAttribute('to')) self.assertEquals("unavailable", presence.getAttribute('type')) class AvailabilityPresenceTest(unittest.TestCase): def test_fromElement(self): xml = """ chat Let's chat! 50 """ presence = xmppim.AvailabilityPresence.fromElement(parseXml(xml)) self.assertEquals(JID('user@example.org'), presence.sender) self.assertEquals(JID('user@example.com'), presence.recipient) self.assertTrue(presence.available) self.assertEquals('chat', presence.show) self.assertEquals({None: "Let's chat!"}, presence.statuses) self.assertEquals(50, presence.priority) class PresenceProtocolTest(unittest.TestCase): """ Tests for L{xmppim.PresenceProtocol} """ def setUp(self): self.output = [] self.protocol = xmppim.PresenceProtocol() self.protocol.parent = self self.protocol.xmlstream = utility.EventDispatcher() self.protocol.connectionInitialized() def send(self, obj): self.output.append(obj) def test_errorReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def errorReceived(error): xmppim.PresenceProtocol.errorReceived(self.protocol, error) try: self.assertIsInstance(error, ErrorStanza) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.errorReceived = errorReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_availableReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def availableReceived(presence): xmppim.PresenceProtocol.availableReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.AvailabilityPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.availableReceived = availableReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_unavailableReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def unavailableReceived(presence): xmppim.PresenceProtocol.unavailableReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.AvailabilityPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.unavailableReceived = unavailableReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_subscribeReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def subscribeReceived(presence): xmppim.PresenceProtocol.subscribeReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.SubscriptionPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.subscribeReceived = subscribeReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_unsubscribeReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def unsubscribeReceived(presence): xmppim.PresenceProtocol.unsubscribeReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.SubscriptionPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.unsubscribeReceived = unsubscribeReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_subscribedReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def subscribedReceived(presence): xmppim.PresenceProtocol.subscribedReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.SubscriptionPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.subscribedReceived = subscribedReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_unsubscribedReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def unsubscribedReceived(presence): xmppim.PresenceProtocol.unsubscribedReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.SubscriptionPresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.unsubscribedReceived = unsubscribedReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_probeReceived(self): """ Incoming presence stanzas are parsed and dispatched. """ xml = """""" def probeReceived(presence): xmppim.PresenceProtocol.probeReceived(self.protocol, presence) try: self.assertIsInstance(presence, xmppim.ProbePresence) except: d.errback() else: d.callback(None) d = defer.Deferred() self.protocol.probeReceived = probeReceived self.protocol.xmlstream.dispatch(parseXml(xml)) return d def test_available(self): """ It should be possible to pass a sender address. """ self.protocol.available(JID('user@example.com'), show=u'chat', status=u'Talk to me!', priority=50) element = self.output[-1] self.assertEquals("user@example.com", element.getAttribute('to')) self.assertIdentical(None, element.getAttribute('type')) self.assertEquals(u'chat', unicode(element.show)) self.assertEquals(u'Talk to me!', unicode(element.status)) self.assertEquals(u'50', unicode(element.priority)) def test_availableLanguages(self): """ It should be possible to pass a sender address. """ self.protocol.available(JID('user@example.com'), show=u'chat', statuses={None: u'Talk to me!', 'nl': u'Praat met me!'}, priority=50) element = self.output[-1] self.assertEquals("user@example.com", element.getAttribute('to')) self.assertIdentical(None, element.getAttribute('type')) self.assertEquals(u'chat', unicode(element.show)) statuses = {} for status in element.elements(): if status.name == 'status': lang = status.getAttribute((NS_XML, 'lang')) statuses[lang] = unicode(status) self.assertIn(None, statuses) self.assertEquals(u'Talk to me!', statuses[None]) self.assertIn('nl', statuses) self.assertEquals(u'Praat met me!', statuses['nl']) self.assertEquals(u'50', unicode(element.priority)) def test_availableSender(self): """ It should be possible to pass a sender address. """ self.protocol.available(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_unavailableDirected(self): """ Test sending of directed unavailable presence broadcast. """ self.protocol.unavailable(JID('user@example.com')) element = self.output[-1] self.assertEquals("presence", element.name) self.assertEquals(None, element.uri) self.assertEquals("user@example.com", element.getAttribute('to')) self.assertEquals("unavailable", element.getAttribute('type')) def test_unavailableWithStatus(self): """ Test sending of directed unavailable presence broadcast with status. """ self.protocol.unavailable(JID('user@example.com'), {None: 'Disconnected'}) element = self.output[-1] self.assertEquals("presence", element.name) self.assertEquals(None, element.uri) self.assertEquals("user@example.com", element.getAttribute('to')) self.assertEquals("unavailable", element.getAttribute('type')) self.assertEquals("Disconnected", unicode(element.status)) def test_unavailableBroadcast(self): """ Test sending of unavailable presence broadcast. """ self.protocol.unavailable(None) element = self.output[-1] self.assertEquals("presence", element.name) self.assertEquals(None, element.uri) self.assertEquals(None, element.getAttribute('to')) self.assertEquals("unavailable", element.getAttribute('type')) def test_unavailableBroadcastNoRecipientParameter(self): """ Test sending of unavailable presence broadcast by not passing entity. """ self.protocol.unavailable() element = self.output[-1] self.assertEquals("presence", element.name) self.assertEquals(None, element.uri) self.assertEquals(None, element.getAttribute('to')) self.assertEquals("unavailable", element.getAttribute('type')) def test_unavailableSender(self): """ It should be possible to pass a sender address. """ self.protocol.unavailable(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_subscribeSender(self): """ It should be possible to pass a sender address. """ self.protocol.subscribe(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_unsubscribeSender(self): """ It should be possible to pass a sender address. """ self.protocol.unsubscribe(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_subscribedSender(self): """ It should be possible to pass a sender address. """ self.protocol.subscribed(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_unsubscribedSender(self): """ It should be possible to pass a sender address. """ self.protocol.unsubscribed(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) def test_probeSender(self): """ It should be possible to pass a sender address. """ self.protocol.probe(JID('user@example.com'), sender=JID('user@example.org')) element = self.output[-1] self.assertEquals("user@example.org", element.getAttribute('from')) class RosterItemTest(unittest.TestCase): """ Tests for L{xmppim.RosterItem}. """ def test_toElement(self): """ A roster item has the correct namespace/name, lacks unset attributes. """ item = xmppim.RosterItem(JID('user@example.org')) element = item.toElement() self.assertEqual('item', element.name) self.assertEqual(NS_ROSTER, element.uri) self.assertFalse(element.hasAttribute('subscription')) self.assertFalse(element.hasAttribute('ask')) self.assertEqual(u"", element.getAttribute('name', u"")) self.assertFalse(element.hasAttribute('approved')) self.assertEquals(0, len(list(element.elements()))) def test_toElementMinimal(self): """ A bare roster item only has a jid attribute. """ item = xmppim.RosterItem(JID('user@example.org')) element = item.toElement() self.assertEqual(u'user@example.org', element.getAttribute('jid')) def test_toElementSubscriptionNone(self): """ A roster item with no subscription has no subscription attribute. """ item = xmppim.RosterItem(JID('user@example.org'), subscriptionTo=False, subscriptionFrom=False) element = item.toElement() self.assertIdentical(None, element.getAttribute('subscription')) def test_toElementSubscriptionTo(self): """ A roster item with subscriptionTo set has subscription 'to'. """ item = xmppim.RosterItem(JID('user@example.org'), subscriptionTo=True, subscriptionFrom=False) element = item.toElement() self.assertEqual('to', element.getAttribute('subscription')) def test_toElementSubscriptionFrom(self): """ A roster item with subscriptionFrom set has subscription 'to'. """ item = xmppim.RosterItem(JID('user@example.org'), subscriptionTo=False, subscriptionFrom=True) element = item.toElement() self.assertEqual('from', element.getAttribute('subscription')) def test_toElementSubscriptionBoth(self): """ A roster item with mutual subscription has subscription 'both'. """ item = xmppim.RosterItem(JID('user@example.org'), subscriptionTo=True, subscriptionFrom=True) element = item.toElement() self.assertEqual('both', element.getAttribute('subscription')) def test_toElementSubscriptionRemove(self): """ A roster item with remove set has subscription 'remove'. """ item = xmppim.RosterItem(JID('user@example.org')) item.remove = True element = item.toElement() self.assertEqual('remove', element.getAttribute('subscription')) def test_toElementAsk(self): """ A roster item with pendingOut set has subscription 'ask'. """ item = xmppim.RosterItem(JID('user@example.org')) item.pendingOut = True element = item.toElement() self.assertEqual('subscribe', element.getAttribute('ask')) def test_toElementName(self): """ A roster item's name is rendered to the 'name' attribute. """ item = xmppim.RosterItem(JID('user@example.org'), name='Joe User') element = item.toElement() self.assertEqual(u'Joe User', element.getAttribute('name')) def test_toElementGroups(self): """ A roster item's groups are rendered as 'group' child elements. """ groups = set(['Friends', 'Jabber']) item = xmppim.RosterItem(JID('user@example.org'), groups=groups) element = item.toElement() foundGroups = set() for child in element.elements(): if child.uri == NS_ROSTER and child.name == 'group': foundGroups.add(unicode(child)) self.assertEqual(groups, foundGroups) def test_toElementApproved(self): """ A pre-approved subscription for a roster item has an 'approved' flag. """ item = xmppim.RosterItem(JID('user@example.org')) item.approved = True element = item.toElement() self.assertEqual(u'true', element.getAttribute('approved')) def test_fromElementMinimal(self): """ A minimal roster item has a reference to the JID of the contact. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertEqual(JID(u"test@example.org"), item.entity) self.assertEqual(u"", item.name) self.assertFalse(item.subscriptionTo) self.assertFalse(item.subscriptionFrom) self.assertFalse(item.pendingOut) self.assertFalse(item.approved) self.assertEqual(set(), item.groups) def test_fromElementName(self): """ A roster item may have an optional name. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertEqual(u"Test User", item.name) def test_fromElementGroups(self): """ A roster item may have one or more groups. """ xml = """ Friends Twisted """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertIn(u"Twisted", item.groups) self.assertIn(u"Friends", item.groups) def test_fromElementSubscriptionNone(self): """ Subscription 'none' sets both attributes to False. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertFalse(item.remove) self.assertFalse(item.subscriptionTo) self.assertFalse(item.subscriptionFrom) def test_fromElementSubscriptionTo(self): """ Subscription 'to' sets the corresponding attribute to True. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertFalse(item.remove) self.assertTrue(item.subscriptionTo) self.assertFalse(item.subscriptionFrom) def test_fromElementSubscriptionFrom(self): """ Subscription 'from' sets the corresponding attribute to True. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertFalse(item.remove) self.assertFalse(item.subscriptionTo) self.assertTrue(item.subscriptionFrom) def test_fromElementSubscriptionBoth(self): """ Subscription 'both' sets both attributes to True. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertFalse(item.remove) self.assertTrue(item.subscriptionTo) self.assertTrue(item.subscriptionFrom) def test_fromElementSubscriptionRemove(self): """ Subscription 'remove' sets the remove attribute. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertTrue(item.remove) def test_fromElementPendingOut(self): """ The ask attribute, if set to 'subscription', means pending out. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertTrue(item.pendingOut) def test_fromElementApprovedTrue(self): """ The approved attribute (true) signals a pre-approved subscription. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertTrue(item.approved) def test_fromElementApproved1(self): """ The approved attribute (1) signals a pre-approved subscription. """ xml = """ """ item = xmppim.RosterItem.fromElement(parseXml(xml)) self.assertTrue(item.approved) def test_jidDeprecationGet(self): """ Getting the jid attribute works as entity and warns deprecation. """ item = xmppim.RosterItem(JID('user@example.org')) entity = self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterItem.jid was " "deprecated in Wokkel 0.7.1; " "please use RosterItem.entity instead.", xmppim.__file__, getattr, item, 'jid') self.assertIdentical(entity, item.entity) def test_jidDeprecationSet(self): """ Setting the jid attribute works as entity and warns deprecation. """ item = xmppim.RosterItem(JID('user@example.org')) self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterItem.jid was deprecated " "in Wokkel 0.7.1; " "please use RosterItem.entity instead.", xmppim.__file__, setattr, item, 'jid', JID('other@example.org')) self.assertEqual(JID('other@example.org'), item.entity) def test_askDeprecationGet(self): """ Getting the ask attribute works as entity and warns deprecation. """ item = xmppim.RosterItem(JID('user@example.org')) item.pendingOut = True ask = self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterItem.ask was " "deprecated in Wokkel 0.7.1; " "please use RosterItem.pendingOut instead.", xmppim.__file__, getattr, item, 'ask') self.assertTrue(ask) def test_askDeprecationSet(self): """ Setting the ask attribute works as entity and warns deprecation. """ item = xmppim.RosterItem(JID('user@example.org')) self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterItem.ask was " "deprecated in Wokkel 0.7.1; " "please use RosterItem.pendingOut instead.", xmppim.__file__, setattr, item, 'ask', True) self.assertTrue(item.pendingOut) class RosterRequestTest(unittest.TestCase): """ Tests for L{xmppim.RosterRequest}. """ def test_fromElement(self): """ A bare roster request is parsed and missing information is None. """ xml = """ """ request = xmppim.RosterRequest.fromElement(parseXml(xml)) self.assertEqual('get', request.stanzaType) self.assertEqual(JID('this@example.org/Home'), request.recipient) self.assertEqual(JID('this@example.org'), request.sender) self.assertEqual(None, request.item) self.assertEqual(None, request.version) def test_fromElementItem(self): """ If an item is present, parse it and put it in the request item. """ xml = """ """ request = xmppim.RosterRequest.fromElement(parseXml(xml)) self.assertNotIdentical(None, request.item) self.assertEqual(JID('user@example.org'), request.item.entity) def test_fromElementVersion(self): """ If a ver attribute is present, put it in the request version. """ xml = """ """ request = xmppim.RosterRequest.fromElement(parseXml(xml)) self.assertEqual('ver72', request.version) def test_fromElementVersionEmpty(self): """ The ver attribute may be empty. """ xml = """ """ request = xmppim.RosterRequest.fromElement(parseXml(xml)) self.assertEqual('', request.version) def test_toElement(self): """ A roster request has a query element in the roster namespace. """ request = xmppim.RosterRequest() element = request.toElement() children = element.elements() child = children.next() self.assertEqual(NS_ROSTER, child.uri) self.assertEqual('query', child.name) def test_toElementItem(self): """ If an item is set, it is rendered as a child of the query. """ request = xmppim.RosterRequest() request.item = xmppim.RosterItem(JID('user@example.org')) element = request.toElement() children = element.query.elements() child = children.next() self.assertEqual(NS_ROSTER, child.uri) self.assertEqual('item', child.name) class FakeClient(object): """ Fake client stream manager for roster tests. """ def __init__(self, xmlstream, jid): self.xmlstream = xmlstream self.jid = jid def request(self, request): element = request.toElement() self.xmlstream.send(element) return defer.Deferred() def addHandler(self, handler): handler.makeConnection(self.xmlstream) handler.connectionInitialized() def test_toElementVersion(self): """ If the roster version is set, a 'ver' attribute is added. """ request = xmppim.RosterRequest() request.version = 'ver72' element = request.toElement() self.assertEqual('ver72', element.query.getAttribute('ver')) def test_toElementVersionEmpty(self): """ If the roster version is the empty string, it should add 'ver', too. """ request = xmppim.RosterRequest() request.version = '' element = request.toElement() self.assertEqual('', element.query.getAttribute('ver')) class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin): """ Tests for L{xmppim.RosterClientProtocol}. """ def setUp(self): self.stub = XmlStreamStub() self.client = FakeClient(self.stub.xmlstream, JID('this@example.org')) self.service = xmppim.RosterClientProtocol() self.service.setHandlerParent(self.client) def test_setItem(self): """ Setting a roster item renders the item and sends it out. """ item = xmppim.RosterItem(JID('test@example.org'), name='Joe User', groups=set(['Friends', 'Jabber'])) d = self.service.setItem(item) # Inspect outgoing iq request iq = self.stub.output[-1] self.assertEqual('set', iq.getAttribute('type')) self.assertNotIdentical(None, iq.query) self.assertEqual(NS_ROSTER, iq.query.uri) children = list(domish.generateElementsQNamed(iq.query.children, 'item', NS_ROSTER)) self.assertEqual(1, len(children)) child = children[0] self.assertEqual('test@example.org', child['jid']) self.assertIdentical(None, child.getAttribute('subscription')) # Fake successful response response = toResponse(iq, 'result') d.callback(response) return d def test_setItemIgnoreAttributes(self): """ Certain attributes should be rendered for roster set. """ item = xmppim.RosterItem(JID('test@example.org'), subscriptionTo=True, subscriptionFrom=False, name='Joe User', groups=set(['Friends', 'Jabber'])) item.pendingOut = True item.approved = True d = self.service.setItem(item) # Inspect outgoing iq request iq = self.stub.output[-1] self.assertEqual('set', iq.getAttribute('type')) self.assertNotIdentical(None, iq.query) self.assertEqual(NS_ROSTER, iq.query.uri) children = list(domish.generateElementsQNamed(iq.query.children, 'item', NS_ROSTER)) self.assertEqual(1, len(children)) child = children[0] self.assertIdentical(None, child.getAttribute('ask')) self.assertIdentical(None, child.getAttribute('approved')) self.assertIdentical(None, child.getAttribute('subscription')) # Fake successful response response = toResponse(iq, 'result') d.callback(response) return d def test_removeItem(self): """ Removing a roster item is setting an item with subscription C{remove}. """ d = self.service.removeItem(JID('test@example.org')) # Inspect outgoing iq request iq = self.stub.output[-1] self.assertEqual('set', iq.getAttribute('type')) self.assertNotIdentical(None, iq.query) self.assertEqual(NS_ROSTER, iq.query.uri) children = list(domish.generateElementsQNamed(iq.query.children, 'item', NS_ROSTER)) self.assertEqual(1, len(children)) child = children[0] self.assertEqual('test@example.org', child['jid']) self.assertEqual('remove', child.getAttribute('subscription')) # Fake successful response response = toResponse(iq, 'result') d.callback(response) return d def test_getRoster(self): """ A request for the roster is sent out and the response is parsed. """ def cb(roster): self.assertIn(JID('user@example.org'), roster) self.assertIdentical(None, getattr(roster, 'version')) d = self.service.getRoster() d.addCallback(cb) # Inspect outgoing iq request iq = self.stub.output[-1] self.assertEqual('get', iq.getAttribute('type')) self.assertNotIdentical(None, iq.query) self.assertEqual(NS_ROSTER, iq.query.uri) self.assertFalse(iq.query.hasAttribute('ver')) # Fake successful response response = toResponse(iq, 'result') query = response.addElement((NS_ROSTER, 'query')) item = query.addElement('item') item['jid'] = 'user@example.org' d.callback(response) return d def test_getRosterVer(self): """ A request for the roster with version passes the version on. """ def cb(roster): self.assertEqual('ver96', getattr(roster, 'version')) d = self.service.getRoster(version='ver72') d.addCallback(cb) # Inspect outgoing iq request iq = self.stub.output[-1] self.assertEqual('ver72', iq.query.getAttribute('ver')) # Fake successful response response = toResponse(iq, 'result') query = response.addElement((NS_ROSTER, 'query')) query['ver'] = 'ver96' item = query.addElement('item') item['jid'] = 'user@example.org' d.callback(response) return d def test_getRosterVerEmptyResult(self): """ An empty response is returned as None. """ def cb(response): self.assertIdentical(None, response) d = self.service.getRoster(version='ver72') d.addCallback(cb) # Inspect outgoing iq request iq = self.stub.output[-1] # Fake successful response response = toResponse(iq, 'result') d.callback(response) return d def test_onRosterSet(self): """ A roster push causes onRosterSet to be called with the parsed item. """ xml = """ """ items = [] def onRosterSet(item): items.append(item) def cb(result): self.assertEqual(1, len(items)) self.assertEqual(JID('user@example.org'), items[0].entity) self.service.onRosterSet = onRosterSet d = self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterClientProtocol.onRosterSet " "was deprecated in Wokkel 0.7.1; " "please use RosterClientProtocol.setReceived " "instead.", xmppim.__file__, self.handleRequest, xml) d.addCallback(cb) return d def test_onRosterRemove(self): """ A roster push causes onRosterSet to be called with the parsed item. """ xml = """ """ entities = [] def onRosterRemove(entity): entities.append(entity) def cb(result): self.assertEqual([JID('user@example.org')], entities) self.service.onRosterRemove = onRosterRemove d = self.assertWarns(DeprecationWarning, "wokkel.xmppim.RosterClientProtocol.onRosterRemove " "was deprecated in Wokkel 0.7.1; " "please use RosterClientProtocol.removeReceived " "instead.", xmppim.__file__, self.handleRequest, xml) d.addCallback(cb) return d def test_setReceived(self): """ A roster set push causes setReceived. """ xml = """ """ requests = [] def setReceived(request): requests.append(request) def cb(result): self.assertEqual(1, len(requests), "setReceived was not called") self.assertEqual(JID('user@example.org'), requests[0].item.entity) self.service.setReceived = setReceived d = self.handleRequest(xml) d.addCallback(cb) return d def test_setReceivedOtherSource(self): """ Roster pushes can be sent from other entities, too, ignore them. """ xml = """ """ def cb(result): self.assertEquals('service-unavailable', result.condition) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_setReceivedOtherSourceAllowed(self): """ Roster pushes can be sent from other entities, allow them. """ xml = """ """ self.service.allowAnySender = True requests = [] def setReceived(request): requests.append(request) def cb(result): self.assertEqual(1, len(requests), "setReceived was not called") self.service.setReceived = setReceived d = self.handleRequest(xml) d.addCallback(cb) return d def test_setReceivedOtherSourceIgnored(self): """ Roster pushes can be sent from other entities, allow them. """ xml = """ """ self.service.allowAnySender = True def setReceived(request): if request.sender == JID('bad@example.org'): raise xmppim.RosterPushIgnored() def cb(result): self.assertEquals('service-unavailable', result.condition) self.service.setReceived = setReceived d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_removeReceived(self): """ A roster remove push causes removeReceived. """ xml = """ """ requests = [] def removeReceived(request): requests.append(request) def cb(result): self.assertEqual(1, len(requests), "removeReceived was not called") self.assertEqual(JID('user@example.org'), requests[0].item.entity) self.service.removeReceived = removeReceived d = self.handleRequest(xml) d.addCallback(cb) return d wokkel-0.7.1/wokkel/test/test_iwokkel.py0000664000175000017500000000313211707274572021151 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.iwokkel} """ from twisted.trial import unittest class DeprecationTest(unittest.TestCase): """ Deprecation test for L{wokkel.subprotocols}. """ def lookForDeprecationWarning(self, testmethod, attributeName, newName): """ Importing C{testmethod} emits a deprecation warning. """ warningsShown = self.flushWarnings([testmethod]) self.assertEqual(len(warningsShown), 1) self.assertIdentical(warningsShown[0]['category'], DeprecationWarning) self.assertEqual( warningsShown[0]['message'], "wokkel.iwokkel." + attributeName + " " "was deprecated in Wokkel 0.7.0: Use " + newName + " instead.") def test_iXMPPHandler(self): """ L{wokkel.iwokkel.IXMPPHandler} is deprecated. """ from wokkel.iwokkel import IXMPPHandler IXMPPHandler self.lookForDeprecationWarning( self.test_iXMPPHandler, "IXMPPHandler", "twisted.words.protocols.jabber.ijabber." "IXMPPHandler") def test_iXMPPHandlerCollection(self): """ L{wokkel.iwokkel.IXMPPHandlerCollection} is deprecated. """ from wokkel.iwokkel import IXMPPHandlerCollection IXMPPHandlerCollection self.lookForDeprecationWarning( self.test_iXMPPHandlerCollection, "IXMPPHandlerCollection", "twisted.words.protocols.jabber.ijabber." "IXMPPHandlerCollection") wokkel-0.7.1/wokkel/test/test_delay.py0000664000175000017500000001463711707215356020611 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.delay}. """ from datetime import datetime import dateutil.tz from twisted.trial import unittest from twisted.words.protocols.jabber.jid import JID from wokkel.delay import Delay, DelayMixin from wokkel.generic import Stanza, parseXml class DelayTest(unittest.TestCase): """ Tests for L{delay.Delay}. """ def test_toElement(self): """ The DOM structure has the serialized timestamp and sender address. """ delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25, tzinfo=dateutil.tz.tzutc()), sender=JID(u'user@example.org')) element = delay.toElement() self.assertEqual(u'urn:xmpp:delay', element.uri) self.assertEqual(u'delay', element.name) self.assertEqual(u'2002-09-10T23:08:25Z', element.getAttribute('stamp')) self.assertEqual(u'user@example.org', element.getAttribute('from')) def test_toElementStampMissing(self): """ To render to XML, at least a timestamp must be provided. """ delay = Delay(stamp=None) self.assertRaises(ValueError, delay.toElement) def test_toElementStampOffsetNaive(self): """ The provided timestamp must be offset aware. """ delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25)) self.assertRaises(ValueError, delay.toElement) def test_toElementLegacy(self): """ The legacy format uses C{CCYYMMDDThh:mm:ss} in the old namespace. """ delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25, tzinfo=dateutil.tz.tzutc()), sender=JID(u'user@example.org')) element = delay.toElement(legacy=True) self.assertEqual(u'jabber:x:delay', element.uri) self.assertEqual(u'x', element.name) self.assertEqual(u'20020910T23:08:25', element.getAttribute('stamp')) self.assertEqual(u'user@example.org', element.getAttribute('from')) def test_fromElement(self): """ The timestamp is parsed with the proper timezone (UTC). """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertEqual(datetime(2002, 9, 10, 23, 8, 25, tzinfo=dateutil.tz.tzutc()), delay.stamp) self.assertIdentical(None, delay.sender) def test_fromElementLegacy(self): """ For legacy XEP-0091 support, the timestamp is assumed to be in UTC. """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertEqual(datetime(2002, 9, 10, 23, 8, 25, tzinfo=dateutil.tz.tzutc()), delay.stamp) self.assertIdentical(None, delay.sender) def test_fromElementSender(self): """ The optional original sender address is parsed as a JID. """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertEqual(JID(u'user@example.org'), delay.sender) def test_fromElementSenderBad(self): """ An invalid original sender address results in C{None}. """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertIdentical(None, delay.sender) def test_fromElementMissingStamp(self): """ A missing timestamp results in C{None} for the stamp attribute. """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertIdentical(None, delay.stamp) def test_fromElementBadStamp(self): """ A malformed timestamp results in C{None} for the stamp attribute. """ xml = parseXml(u""" """) delay = Delay.fromElement(xml) self.assertIdentical(None, delay.stamp) class DelayStanza(Stanza, DelayMixin): """ Test stanza class that mixes in delayed delivery information parsing. """ class DelayMixinTest(unittest.TestCase): def test_fromParentElement(self): """ A child element with delay information is found and parsed. """ xml = parseXml(u""" """) stanza = DelayStanza.fromElement(xml) self.assertNotIdentical(None, stanza.delay) def test_fromParentElementLegacy(self): """ A child element with legacy delay information is found and parsed. """ xml = parseXml(u""" """) stanza = DelayStanza.fromElement(xml) self.assertNotIdentical(None, stanza.delay) def test_fromParentElementBothLegacyLast(self): """ The XEP-0203 format is used over later legacy XEP-0091 format. """ xml = parseXml(u""" """) stanza = DelayStanza.fromElement(xml) self.assertNotIdentical(None, stanza.delay) self.assertEqual(2002, stanza.delay.stamp.year) def test_fromParentElementBothLegacyFirst(self): """ The XEP-0203 format is used over earlier legacy XEP-0091 format. """ xml = parseXml(u""" """) stanza = DelayStanza.fromElement(xml) self.assertNotIdentical(None, stanza.delay) self.assertEqual(2002, stanza.delay.stamp.year) wokkel-0.7.1/wokkel/test/test_muc.py0000664000175000017500000017204411707215356020274 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.muc} """ from datetime import datetime from dateutil.tz import tzutc from zope.interface import verify from twisted.trial import unittest from twisted.internet import defer, task from twisted.words.xish import domish, xpath from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.error import StanzaError from twisted.words.protocols.jabber.xmlstream import TimeoutError, toResponse from wokkel import data_form, delay, iwokkel, muc from wokkel.generic import parseXml from wokkel.test.helpers import TestableStreamManager NS_MUC_ADMIN = 'http://jabber.org/protocol/muc#admin' def calledAsync(fn): """ Function wrapper that fires a deferred upon calling the given function. """ d = defer.Deferred() def func(*args, **kwargs): try: result = fn(*args, **kwargs) except: d.errback() else: d.callback(result) return d, func class StatusCodeTest(unittest.TestCase): """ Tests for L{muc.STATUS_CODE}. """ def test_lookupByValue(self): """ The registered MUC status codes map to STATUS_CODE value constants. Note: the identifiers used in the dictionary of status codes are borrowed from U{XEP-0306} that defines Extensible Status Conditions for Multi-User Chat. If this specification is implemented itself, the dictionary could move there. """ codes = { 100: 'realjid-public', 101: 'affiliation-changed', 102: 'unavailable-shown', 103: 'unavailable-not-shown', 104: 'configuration-changed', 110: 'self-presence', 170: 'logging-enabled', 171: 'logging-disabled', 172: 'non-anonymous', 173: 'semi-anonymous', 174: 'fully-anonymous', 201: 'room-created', 210: 'nick-assigned', 301: 'banned', 303: 'new-nick', 307: 'kicked', 321: 'removed-affiliation', 322: 'removed-membership', 332: 'removed-shutdown', } for code, condition in codes.iteritems(): constantName = condition.replace('-', '_').upper() self.assertEqual(getattr(muc.STATUS_CODE, constantName), muc.STATUS_CODE.lookupByValue(code)) class StatusesTest(unittest.TestCase): """ Tests for L{muc.Statuses}. """ def setUp(self): self.mucStatuses = muc.Statuses() self.mucStatuses.add(muc.STATUS_CODE.SELF_PRESENCE) self.mucStatuses.add(muc.STATUS_CODE.ROOM_CREATED) def test_interface(self): """ Instances of L{Statuses} provide L{iwokkel.IMUCStatuses}. """ verify.verifyObject(iwokkel.IMUCStatuses, self.mucStatuses) def test_contains(self): """ The status contained are 'in' the container. """ self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, self.mucStatuses) self.assertIn(muc.STATUS_CODE.ROOM_CREATED, self.mucStatuses) self.assertNotIn(muc.STATUS_CODE.NON_ANONYMOUS, self.mucStatuses) def test_iter(self): """ All statuses can be iterated over. """ statuses = set() for status in self.mucStatuses: statuses.add(status) self.assertEqual(set([muc.STATUS_CODE.SELF_PRESENCE, muc.STATUS_CODE.ROOM_CREATED]), statuses) def test_len(self): """ The number of items in this container is returned by C{__len__}. """ self.assertEqual(2, len(self.mucStatuses)) class GroupChatTest(unittest.TestCase): """ Tests for L{muc.GroupChat}. """ def test_toElementDelay(self): """ If the delay attribute is set, toElement has it rendered. """ message = muc.GroupChat() message.delay = delay.Delay(stamp=datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())) element = message.toElement() query = "/message/delay[@xmlns='%s']" % (delay.NS_DELAY,) nodes = xpath.queryForNodes(query, element) self.assertNotIdentical(None, nodes, "Missing delay element") def test_toElementDelayLegacy(self): """ If legacy delay is requested, the legacy format is rendered. """ message = muc.GroupChat() message.delay = delay.Delay(stamp=datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())) element = message.toElement(legacyDelay=True) query = "/message/x[@xmlns='%s']" % (delay.NS_JABBER_DELAY,) nodes = xpath.queryForNodes(query, element) self.assertNotIdentical(None, nodes, "Missing legacy delay element") class HistoryOptionsTest(unittest.TestCase): """ Tests for L{muc.HistoryOptionsTest}. """ def test_toElement(self): """ toElement renders the history element in the right namespace. """ history = muc.HistoryOptions() element = history.toElement() self.assertEqual(muc.NS_MUC, element.uri) self.assertEqual('history', element.name) def test_toElementMaxStanzas(self): """ If C{maxStanzas} is set, the element has the attribute C{'maxstanzas'}. """ history = muc.HistoryOptions(maxStanzas=10) element = history.toElement() self.assertEqual(u'10', element.getAttribute('maxstanzas')) def test_toElementSince(self): """ If C{since} is set, the attribute C{'since'} has a rendered timestamp. """ history = muc.HistoryOptions(since=datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())) element = history.toElement() self.assertEqual(u'2002-10-13T23:58:37Z', element.getAttribute('since')) class UserPresenceTest(unittest.TestCase): """ Tests for L{muc.UserPresence}. """ def test_fromElementNoUserElement(self): """ Without user element, all associated attributes are None. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertIdentical(None, presence.affiliation) self.assertIdentical(None, presence.role) self.assertIdentical(None, presence.entity) self.assertIdentical(None, presence.nick) self.assertEqual(0, len(presence.mucStatuses)) def test_fromElementUnknownChild(self): """ Unknown child elements are ignored. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertEqual(0, len(presence.mucStatuses)) def test_fromElementStatusOne(self): """ Status codes are extracted. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, presence.mucStatuses) def test_fromElementStatusMultiple(self): """ Multiple status codes are all extracted. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, presence.mucStatuses) self.assertIn(muc.STATUS_CODE.REALJID_PUBLIC, presence.mucStatuses) def test_fromElementStatusEmpty(self): """ Empty status elements are ignored. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertEqual(0, len(presence.mucStatuses)) def test_fromElementStatusBad(self): """ Bad status codes are ignored. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertEqual(0, len(presence.mucStatuses)) def test_fromElementStatusUnknown(self): """ Unknown status codes are not recorded in C{mucStatuses}. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertEqual(0, len(presence.mucStatuses)) def test_fromElementItem(self): """ Item attributes are parsed properly. """ xml = """ """ element = parseXml(xml) presence = muc.UserPresence.fromElement(element) self.assertEqual(u'member', presence.affiliation) self.assertEqual(u'participant', presence.role) self.assertEqual(JID('hag66@shakespeare.lit/pda'), presence.entity) self.assertEqual(u'thirdwitch', presence.nick) class MUCClientProtocolTest(unittest.TestCase): """ Tests for L{muc.MUCClientProtocol}. """ def setUp(self): self.clock = task.Clock() self.sessionManager = TestableStreamManager(reactor=self.clock) self.stub = self.sessionManager.stub self.protocol = muc.MUCClientProtocol(reactor=self.clock) self.protocol.setHandlerParent(self.sessionManager) self.roomIdentifier = 'test' self.service = 'conference.example.org' self.nick = 'Nick' self.occupantJID = JID(tuple=(self.roomIdentifier, self.service, self.nick)) self.roomJID = self.occupantJID.userhostJID() self.userJID = JID('test@example.org/Testing') def test_initNoReactor(self): """ If no reactor is passed, the default reactor is used. """ protocol = muc.MUCClientProtocol() from twisted.internet import reactor self.assertEqual(reactor, protocol._reactor) def test_groupChatReceived(self): """ Messages of type groupchat are parsed and passed to L{groupChatReceived}. """ xml = u""" test """ % (self.occupantJID) def groupChatReceived(message): self.assertEquals('test', message.body, "Wrong group chat message") self.assertEquals(self.roomIdentifier, message.sender.user, 'Wrong room identifier') d, self.protocol.groupChatReceived = calledAsync(groupChatReceived) self.stub.send(parseXml(xml)) return d def test_groupChatReceivedNotOverridden(self): """ If L{groupChatReceived} has not been overridden, no errors should occur. """ xml = u""" test """ % (self.occupantJID) self.stub.send(parseXml(xml)) def test_join(self): """ Joining a room waits for confirmation, deferred fires user presence. """ def cb(presence): self.assertEquals(self.occupantJID, presence.sender) # Join the room d = self.protocol.join(self.roomJID, self.nick) d.addCallback(cb) element = self.stub.output[-1] self.assertEquals('presence', element.name, "Need to be presence") self.assertNotIdentical(None, element.x, 'No muc x element') # send back user presence, they joined xml = """ """ % (self.roomIdentifier, self.service, self.nick) self.stub.send(parseXml(xml)) return d def test_joinHistory(self): """ Passing a history parameter sends a 'maxStanzas' history limit. """ historyOptions = muc.HistoryOptions(maxStanzas=10) d = self.protocol.join(self.roomJID, self.nick, historyOptions) element = self.stub.output[-1] query = "/*/x[@xmlns='%s']/history[@xmlns='%s']" % (muc.NS_MUC, muc.NS_MUC) result = xpath.queryForNodes(query, element) history = result[0] self.assertEquals('10', history.getAttribute('maxstanzas')) # send back user presence, they joined xml = """ """ % (self.roomIdentifier, self.service, self.nick) self.stub.send(parseXml(xml)) return d def test_joinForbidden(self): """ A forbidden error in response to a join errbacks with L{StanzaError}. """ def cb(error): self.assertEquals('forbidden', error.condition, 'Wrong muc condition') d = self.protocol.join(self.roomJID, self.nick) self.assertFailure(d, StanzaError) d.addCallback(cb) # send back error, forbidden xml = u""" """ % (self.occupantJID) self.stub.send(parseXml(xml)) return d def test_joinForbiddenFromRoomJID(self): """ An error response to a join sent from the room JID should errback. Some service implementations send error stanzas from the room JID instead of the JID the join presence was sent to. """ d = self.protocol.join(self.roomJID, self.nick) self.assertFailure(d, StanzaError) # send back error, forbidden xml = u""" """ % (self.roomJID) self.stub.send(parseXml(xml)) return d def test_joinBadJID(self): """ Client joining a room and getting a jid-malformed error. """ def cb(error): self.assertEquals('jid-malformed', error.condition, 'Wrong muc condition') d = self.protocol.join(self.roomJID, self.nick) self.assertFailure(d, StanzaError) d.addCallback(cb) # send back error, bad JID xml = u""" """ % (self.occupantJID) self.stub.send(parseXml(xml)) return d def test_joinTimeout(self): """ After not receiving a response to a join, errback with L{TimeoutError}. """ d = self.protocol.join(self.roomJID, self.nick) self.assertFailure(d, TimeoutError) self.clock.advance(muc.DEFER_TIMEOUT) return d def test_joinPassword(self): """ Sending a password via presence to a password protected room. """ self.protocol.join(self.roomJID, self.nick, password='secret') element = self.stub.output[-1] self.assertTrue(xpath.matches( u"/presence[@to='%s']/x/password" "[text()='secret']" % (self.occupantJID,), element), 'Wrong presence stanza') def test_nick(self): """ Send a nick change to the server. """ newNick = 'newNick' def cb(presence): self.assertEquals(JID(tuple=(self.roomIdentifier, self.service, newNick)), presence.sender) d = self.protocol.nick(self.roomJID, newNick) d.addCallback(cb) element = self.stub.output[-1] self.assertEquals('presence', element.name, "Need to be presence") self.assertNotIdentical(None, element.x, 'No muc x element') # send back user presence, nick changed xml = u""" """ % (self.roomJID, newNick) self.stub.send(parseXml(xml)) return d def test_nickConflict(self): """ If the server finds the new nick in conflict, the errback is called. """ newNick = 'newNick' d = self.protocol.nick(self.roomJID, newNick) self.assertFailure(d, StanzaError) element = self.stub.output[-1] self.assertEquals('presence', element.name, "Need to be presence") self.assertNotIdentical(None, element.x, 'No muc x element') # send back error presence, nick conflicted xml = u""" """ % (self.roomJID, newNick) self.stub.send(parseXml(xml)) return d def test_status(self): """ Change status """ def joined(_): d = self.protocol.status(self.roomJID, 'xa', 'testing MUC') d.addCallback(statusChanged) return d def statusChanged(presence): self.assertEqual(self.occupantJID, presence.sender) # Join the room d = self.protocol.join(self.roomJID, self.nick) d.addCallback(joined) # Receive presence back from the room: joined. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) # The presence for the status change should have been sent now. element = self.stub.output[-1] self.assertEquals('presence', element.name, "Need to be presence") self.assertTrue(getattr(element, 'x', None), 'No muc x element') # send back user presence, status changed xml = u""" xa testing MUC """ % self.occupantJID self.stub.send(parseXml(xml)) return d def test_leave(self): """ Client leaves a room """ def joined(_): return self.protocol.leave(self.roomJID) # Join the room d = self.protocol.join(self.roomJID, self.nick) d.addCallback(joined) # Receive presence back from the room: joined. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) # The presence for leaving the room should have been sent now. element = self.stub.output[-1] self.assertEquals('unavailable', element['type'], 'Unavailable is not being sent') # Receive presence back from the room: left. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) return d def test_groupChat(self): """ Send private messages to muc entities. """ self.protocol.groupChat(self.roomJID, u'This is a test') message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertEquals(self.roomJID.full(), message.getAttribute('to')) self.assertEquals('groupchat', message.getAttribute('type')) self.assertEquals(u'This is a test', unicode(message.body)) def test_chat(self): """ Send private messages to muc entities. """ otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick') self.protocol.chat(otherOccupantJID, u'This is a test') message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertEquals(otherOccupantJID.full(), message.getAttribute('to')) self.assertEquals('chat', message.getAttribute('type')) self.assertEquals(u'This is a test', unicode(message.body)) def test_subject(self): """ Change subject of the room. """ self.protocol.subject(self.roomJID, u'This is a test') message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertEquals(self.roomJID.full(), message.getAttribute('to')) self.assertEquals('groupchat', message.getAttribute('type')) self.assertEquals(u'This is a test', unicode(message.subject)) def test_invite(self): """ Invite a user to a room """ invitee = JID('other@example.org') self.protocol.invite(self.roomJID, invitee, u'This is a test') message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertEquals(self.roomJID.full(), message.getAttribute('to')) self.assertEquals(muc.NS_MUC_USER, message.x.uri) self.assertEquals(muc.NS_MUC_USER, message.x.invite.uri) self.assertEquals(invitee.full(), message.x.invite.getAttribute('to')) self.assertEquals(muc.NS_MUC_USER, message.x.invite.reason.uri) self.assertEquals(u'This is a test', unicode(message.x.invite.reason)) def test_getRegisterForm(self): """ The response of a register form request should extract the form. """ def cb(form): self.assertEquals('form', form.formType) d = self.protocol.getRegisterForm(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % (muc.NS_REGISTER) nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Missing query element') self.assertRaises(StopIteration, nodes[0].elements().next) xml = u""" http://jabber.org/protocol/muc#register """ % (self.roomJID, iq['id'], self.userJID) self.stub.send(parseXml(xml)) return d def test_register(self): """ Client registering with a room. http://xmpp.org/extensions/xep-0045.html#register """ def cb(iq): # check for a result self.assertEquals('result', iq['type'], 'We did not get a result') d = self.protocol.register(self.roomJID, {'muc#register_roomnick': 'thirdwitch'}) d.addCallback(cb) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % muc.NS_REGISTER nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Invalid registration request') form = data_form.findForm(nodes[0], muc.NS_MUC_REGISTER) self.assertNotIdentical(None, form, 'Missing registration form') self.assertEquals('submit', form.formType) self.assertIn('muc#register_roomnick', form.fields) response = toResponse(iq, 'result') self.stub.send(response) return d def test_registerCancel(self): """ Cancelling a registration request sends a cancel form. """ d = self.protocol.register(self.roomJID, None) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % muc.NS_REGISTER nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Invalid registration request') form = data_form.findForm(nodes[0], muc.NS_MUC_REGISTER) self.assertNotIdentical(None, form, 'Missing registration form') self.assertEquals('cancel', form.formType) response = toResponse(iq, 'result') self.stub.send(response) return d def test_voice(self): """ Client requesting voice for a room. """ self.protocol.voice(self.occupantJID) m = self.stub.output[-1] query = ("/message/x[@type='submit']/field/value" "[text()='%s']") % muc.NS_MUC_REQUEST self.assertTrue(xpath.matches(query, m), 'Invalid voice message stanza') def test_history(self): """ Converting a one to one chat to a multi-user chat. """ archive = [] thread = "e0ffe42b28561960c6b12b944a092794b9683a38" # create messages element = domish.Element((None, 'message')) element['to'] = 'testing@example.com' element['type'] = 'chat' element.addElement('body', None, 'test') element.addElement('thread', None, thread) archive.append({'stanza': element, 'timestamp': datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())}) element = domish.Element((None, 'message')) element['to'] = 'testing2@example.com' element['type'] = 'chat' element.addElement('body', None, 'yo') element.addElement('thread', None, thread) archive.append({'stanza': element, 'timestamp': datetime(2002, 10, 13, 23, 58, 43, tzinfo=tzutc())}) self.protocol.history(self.occupantJID, archive) while len(self.stub.output)>0: element = self.stub.output.pop() # check for delay element self.assertEquals('message', element.name, 'Wrong stanza') self.assertTrue(xpath.matches("/message/delay", element), 'Invalid history stanza') def test_getConfiguration(self): """ The response of a configure form request should extract the form. """ def cb(form): self.assertEquals('form', form.formType) d = self.protocol.getConfiguration(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER) nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Missing query element') self.assertRaises(StopIteration, nodes[0].elements().next) xml = u""" http://jabber.org/protocol/muc#roomconfig """ % (self.roomJID, iq['id'], self.userJID) self.stub.send(parseXml(xml)) return d def test_getConfigurationNoOptions(self): """ The response of a configure form request should extract the form. """ def cb(form): self.assertIdentical(None, form) d = self.protocol.getConfiguration(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] xml = u""" """ % (self.roomJID, iq['id'], self.userJID) self.stub.send(parseXml(xml)) return d def test_configure(self): """ Default configure and changing the room name. """ def cb(iq): self.assertEquals('result', iq['type'], 'Not a result') values = {'muc#roomconfig_roomname': self.roomIdentifier} d = self.protocol.configure(self.roomJID, values) d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('set', iq.getAttribute('type')) self.assertEquals(self.roomJID.full(), iq.getAttribute('to')) query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER) nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Bad configure request') form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG) self.assertNotIdentical(None, form, 'Missing configuration form') self.assertEquals('submit', form.formType) response = toResponse(iq, 'result') self.stub.send(response) return d def test_configureEmpty(self): """ Accept default configuration by sending an empty form. """ values = {} d = self.protocol.configure(self.roomJID, values) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER) nodes = xpath.queryForNodes(query, iq) form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG) self.assertNotIdentical(None, form, 'Missing configuration form') self.assertEquals('submit', form.formType) response = toResponse(iq, 'result') self.stub.send(response) return d def test_configureCancel(self): """ Cancelling room configuration should send a cancel form. """ d = self.protocol.configure(self.roomJID, None) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER) nodes = xpath.queryForNodes(query, iq) form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG) self.assertNotIdentical(None, form, 'Missing configuration form') self.assertEquals('cancel', form.formType) response = toResponse(iq, 'result') self.stub.send(response) return d def test_getMemberList(self): """ Retrieving the member list returns a list of L{muc.AdminItem}s The request asks for the affiliation C{'member'}. """ def cb(items): self.assertEquals(1, len(items)) item = items[0] self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity) self.assertEquals(u'thirdwitch', item.nick) self.assertEquals(u'member', item.affiliation) d = self.protocol.getMemberList(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('get', iq.getAttribute('type')) query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, items) self.assertEquals(1, len(items)) self.assertEquals('member', items[0].getAttribute('affiliation')) response = toResponse(iq, 'result') query = response.addElement((NS_MUC_ADMIN, 'query')) item = query.addElement('item') item['affiliation'] ='member' item['jid'] = 'hag66@shakespeare.lit' item['nick'] = 'thirdwitch' item['role'] = 'participant' self.stub.send(response) return d def test_getAdminList(self): """ Retrieving the admin list returns a list of L{muc.AdminItem}s The request asks for the affiliation C{'admin'}. """ d = self.protocol.getAdminList(self.roomJID) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertEquals('admin', items[0].getAttribute('affiliation')) response = toResponse(iq, 'result') query = response.addElement((NS_MUC_ADMIN, 'query')) self.stub.send(response) return d def test_getBanList(self): """ Retrieving the ban list returns a list of L{muc.AdminItem}s The request asks for the affiliation C{'outcast'}. """ def cb(items): self.assertEquals(1, len(items)) item = items[0] self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity) self.assertEquals(u'outcast', item.affiliation) self.assertEquals(u'Trouble making', item.reason) d = self.protocol.getBanList(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertEquals('outcast', items[0].getAttribute('affiliation')) response = toResponse(iq, 'result') query = response.addElement((NS_MUC_ADMIN, 'query')) item = query.addElement('item') item['affiliation'] ='outcast' item['jid'] = 'hag66@shakespeare.lit' item.addElement('reason', content='Trouble making') self.stub.send(response) return d def test_getOwnerList(self): """ Retrieving the owner list returns a list of L{muc.AdminItem}s The request asks for the affiliation C{'owner'}. """ d = self.protocol.getOwnerList(self.roomJID) iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertEquals('owner', items[0].getAttribute('affiliation')) response = toResponse(iq, 'result') query = response.addElement((NS_MUC_ADMIN, 'query')) self.stub.send(response) return d def test_getModeratorList(self): """ Retrieving the moderator returns a list of L{muc.AdminItem}s. The request asks for the role C{'moderator'}. """ def cb(items): self.assertEquals(1, len(items)) item = items[0] self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity) self.assertEquals(u'thirdwitch', item.nick) self.assertEquals(u'moderator', item.role) d = self.protocol.getModeratorList(self.roomJID) d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('get', iq.getAttribute('type')) query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, items) self.assertEquals(1, len(items)) self.assertEquals('moderator', items[0].getAttribute('role')) response = toResponse(iq, 'result') query = response.addElement((NS_MUC_ADMIN, 'query')) item = query.addElement('item') item['affiliation'] ='member' item['jid'] = 'hag66@shakespeare.lit' item['nick'] = 'thirdwitch' item['role'] = 'moderator' self.stub.send(response) return d def test_modifyAffiliationList(self): entities = [JID('user1@test.example.org'), JID('user2@test.example.org')] d = self.protocol.modifyAffiliationList(self.roomJID, entities, 'admin') iq = self.stub.output[-1] query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN, muc.NS_MUC_ADMIN) items = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, items) self.assertEquals(entities[0], JID(items[0].getAttribute('jid'))) self.assertEquals('admin', items[0].getAttribute('affiliation')) self.assertEquals(entities[1], JID(items[1].getAttribute('jid'))) self.assertEquals('admin', items[1].getAttribute('affiliation')) # Send a response to have the deferred fire. response = toResponse(iq, 'result') self.stub.send(response) return d def test_grantVoice(self): """ Granting voice sends request to set role to 'participant'. """ nick = 'TroubleMaker' def cb(give_voice): self.assertTrue(give_voice, 'Did not give voice user') d = self.protocol.grantVoice(self.roomJID, nick, sender=self.userJID) d.addCallback(cb) iq = self.stub.output[-1] query = (u"/iq[@type='set' and @to='%s']/query/item" "[@role='participant']") % self.roomJID self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza') response = toResponse(iq, 'result') self.stub.send(response) return d def test_revokeVoice(self): """ Revoking voice sends request to set role to 'visitor'. """ nick = 'TroubleMaker' d = self.protocol.revokeVoice(self.roomJID, nick, reason="Trouble maker", sender=self.userJID) iq = self.stub.output[-1] query = (u"/iq[@type='set' and @to='%s']/query/item" "[@role='visitor']") % self.roomJID self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza') response = toResponse(iq, 'result') self.stub.send(response) return d def test_grantModerator(self): """ Granting moderator privileges sends request to set role to 'moderator'. """ nick = 'TroubleMaker' d = self.protocol.grantModerator(self.roomJID, nick, sender=self.userJID) iq = self.stub.output[-1] query = (u"/iq[@type='set' and @to='%s']/query/item" "[@role='moderator']") % self.roomJID self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza') response = toResponse(iq, 'result') self.stub.send(response) return d def test_ban(self): """ Ban an entity in a room. """ banned = JID('ban@jabber.org/TroubleMaker') def cb(banned): self.assertTrue(banned, 'Did not ban user') d = self.protocol.ban(self.roomJID, banned, reason='Spam', sender=self.userJID) d.addCallback(cb) iq = self.stub.output[-1] self.assertTrue(xpath.matches( u"/iq[@type='set' and @to='%s']/query/item" "[@affiliation='outcast']" % (self.roomJID,), iq), 'Wrong ban stanza') response = toResponse(iq, 'result') self.stub.send(response) return d def test_kick(self): """ Kick an entity from a room. """ nick = 'TroubleMaker' def cb(kicked): self.assertTrue(kicked, 'Did not kick user') d = self.protocol.kick(self.roomJID, nick, reason='Spam', sender=self.userJID) d.addCallback(cb) iq = self.stub.output[-1] self.assertTrue(xpath.matches( u"/iq[@type='set' and @to='%s']/query/item" "[@role='none']" % (self.roomJID,), iq), 'Wrong kick stanza') response = toResponse(iq, 'result') self.stub.send(response) return d def test_destroy(self): """ Destroy a room. """ d = self.protocol.destroy(self.occupantJID, reason='Time to leave', alternate=JID('other@%s' % self.service), password='secret') iq = self.stub.output[-1] query = ("/iq/query[@xmlns='%s']/destroy[@xmlns='%s']" % (muc.NS_MUC_OWNER, muc.NS_MUC_OWNER)) nodes = xpath.queryForNodes(query, iq) self.assertNotIdentical(None, nodes, 'Bad configure request') destroy = nodes[0] self.assertEquals('Time to leave', unicode(destroy.reason)) response = toResponse(iq, 'result') self.stub.send(response) return d class MUCClientTest(unittest.TestCase): """ Tests for C{muc.MUCClient}. """ def setUp(self): self.clock = task.Clock() self.sessionManager = TestableStreamManager(reactor=self.clock) self.stub = self.sessionManager.stub self.protocol = muc.MUCClient(reactor=self.clock) self.protocol.setHandlerParent(self.sessionManager) self.roomIdentifier = 'test' self.service = 'conference.example.org' self.nick = 'Nick' self.occupantJID = JID(tuple=(self.roomIdentifier, self.service, self.nick)) self.roomJID = self.occupantJID.userhostJID() self.userJID = JID('test@example.org/Testing') def _createRoom(self): """ A helper method to create a test room. """ # create a room room = muc.Room(self.roomJID, self.nick) self.protocol._addRoom(room) return room def test_interface(self): """ Do instances of L{muc.MUCClient} provide L{iwokkel.IMUCClient}? """ verify.verifyObject(iwokkel.IMUCClient, self.protocol) def _testPresence(self, sender='', available=True): """ Helper for presence tests. """ def userUpdatedStatus(room, user, show, status): self.fail("Unexpected call to userUpdatedStatus") def userJoinedRoom(room, user): self.fail("Unexpected call to userJoinedRoom") if available: available = "" else: available = " type='unavailable'" if sender: sender = u" from='%s'" % sender xml = u""" """ % (self.userJID, sender, available) self.protocol.userUpdatedStatus = userUpdatedStatus self.protocol.userJoinedRoom = userJoinedRoom self.stub.send(parseXml(xml)) def test_availableReceivedEmptySender(self): """ Availability presence from empty sender is ignored. """ self._testPresence(sender='') def test_availableReceivedNotInRoom(self): """ Availability presence from unknown entities is ignored. """ otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick') self._testPresence(sender=otherOccupantJID) def test_unavailableReceivedEmptySender(self): """ Availability presence from empty sender is ignored. """ self._testPresence(sender='', available=False) def test_unavailableReceivedNotInRoom(self): """ Availability presence from unknown entities is ignored. """ otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick') self._testPresence(sender=otherOccupantJID, available=False) def test_unavailableReceivedNotInRoster(self): """ Availability presence from unknown entities is ignored. """ room = self._createRoom() user = muc.User(self.nick) room.addUser(user) otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick') self._testPresence(sender=otherOccupantJID, available=False) def test_userJoinedRoom(self): """ Joins by others to a room we're in are passed to userJoinedRoom """ xml = """ """ % (self.userJID.full(), self.occupantJID.full()) # create a room self._createRoom() def userJoinedRoom(room, user): self.assertEquals(self.roomJID, room.roomJID, 'Wrong room name') self.assertTrue(room.inRoster(user), 'User not in roster') d, self.protocol.userJoinedRoom = calledAsync(userJoinedRoom) self.stub.send(parseXml(xml)) return d def test_receivedSubject(self): """ Subject received from a room we're in are passed to receivedSubject. """ xml = u""" test """ % (self.userJID, self.occupantJID) self._createRoom() # add user to room user = muc.User(self.nick) room = self.protocol._getRoom(self.roomJID) room.addUser(user) def receivedSubject(room, user, subject): self.assertEquals('test', subject, "Wrong group chat message") self.assertEquals(self.roomJID, room.roomJID, 'Wrong room name') self.assertEquals(self.nick, user.nick) d, self.protocol.receivedSubject = calledAsync(receivedSubject) self.stub.send(parseXml(xml)) return d def test_receivedSubjectNotOverridden(self): """ Not overriding receivedSubject is ok. """ xml = u""" test """ % (self.userJID, self.occupantJID) self._createRoom() self.stub.send(parseXml(xml)) def test_receivedGroupChat(self): """ Messages received from a room we're in are passed to receivedGroupChat. """ xml = u""" test """ % (self.occupantJID) self._createRoom() def receivedGroupChat(room, user, message): self.assertEquals('test', message.body, "Wrong group chat message") self.assertEquals(self.roomJID, room.roomJID, 'Wrong room name') d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat) self.stub.send(parseXml(xml)) return d def test_receivedGroupChatRoom(self): """ Messages received from the room itself have C{user} set to C{None}. """ xml = u""" test """ % (self.roomJID) self._createRoom() def receivedGroupChat(room, user, message): self.assertIdentical(None, user) d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat) self.stub.send(parseXml(xml)) return d def test_receivedGroupChatNotInRoom(self): """ Messages received from a room we're not in are ignored. """ xml = u""" test """ % (self.occupantJID) def receivedGroupChat(room, user, message): self.fail("Unexpected call to receivedGroupChat") self.protocol.receivedGroupChat = receivedGroupChat self.stub.send(parseXml(xml)) def test_receivedGroupChatNotOverridden(self): """ Not overriding receivedGroupChat is ok. """ xml = u""" test """ % (self.occupantJID) self._createRoom() self.stub.send(parseXml(xml)) def test_join(self): """ Joining a room waits for confirmation, deferred fires room. """ def cb(room): self.assertEqual(self.roomJID, room.roomJID) self.assertFalse(room.locked) d = self.protocol.join(self.roomJID, self.nick) d.addCallback(cb) # send back user presence, they joined xml = """ """ % (self.roomIdentifier, self.service, self.nick) self.stub.send(parseXml(xml)) return d def test_joinLocked(self): """ A new room is locked by default. """ def cb(room): self.assertTrue(room.locked, "Room is not marked as locked") d = self.protocol.join(self.roomJID, self.nick) d.addCallback(cb) # send back user presence, they joined xml = """ """ % (self.roomIdentifier, self.service, self.nick) self.stub.send(parseXml(xml)) return d def test_joinForbidden(self): """ A forbidden error in response to a join errbacks with L{StanzaError}. """ def cb(error): self.assertEquals('forbidden', error.condition, 'Wrong muc condition') self.assertIdentical(None, self.protocol._getRoom(self.roomJID)) d = self.protocol.join(self.roomJID, self.nick) self.assertFailure(d, StanzaError) d.addCallback(cb) # send back error, forbidden xml = u""" """ % (self.occupantJID) self.stub.send(parseXml(xml)) return d def test_userLeftRoom(self): """ Unavailable presence from a participant removes it from the room. """ xml = u""" """ % (self.userJID, self.occupantJID) # create a room self._createRoom() # add user to room user = muc.User(self.nick) room = self.protocol._getRoom(self.roomJID) room.addUser(user) def userLeftRoom(room, user): self.assertEquals(self.roomJID, room.roomJID, 'Wrong room name') self.assertFalse(room.inRoster(user), 'User in roster') d, self.protocol.userLeftRoom = calledAsync(userLeftRoom) self.stub.send(parseXml(xml)) return d def test_receivedHistory(self): """ Receiving history on room join. """ xml = u""" test """ % (self.occupantJID, self.userJID) self._createRoom() def receivedHistory(room, user, message): self.assertEquals('test', message.body, "wrong message body") stamp = datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc()) self.assertEquals(stamp, message.delay.stamp, 'Does not have a history stamp') d, self.protocol.receivedHistory = calledAsync(receivedHistory) self.stub.send(parseXml(xml)) return d def test_receivedHistoryNotOverridden(self): """ Not overriding receivedHistory is ok. """ xml = u""" test """ % (self.occupantJID, self.userJID) self._createRoom() self.stub.send(parseXml(xml)) def test_nickConflict(self): """ If the server finds the new nick in conflict, the errback is called. """ def cb(failure, room): user = room.getUser(otherNick) self.assertNotIdentical(None, user) self.assertEqual(otherJID, user.entity) def joined(room): d = self.protocol.nick(room.roomJID, otherNick) self.assertFailure(d, StanzaError) d.addCallback(cb, room) otherJID = JID('other@example.org/Home') otherNick = 'otherNick' d = self.protocol.join(self.roomJID, self.nick) d.addCallback(joined) # Send back other partipant's presence. xml = u""" """ % (self.roomJID, otherNick, otherJID) self.stub.send(parseXml(xml)) # send back user presence, they joined xml = u""" """ % (self.roomJID, self.nick) self.stub.send(parseXml(xml)) # send back error presence, nick conflicted xml = u""" """ % (self.roomJID, otherNick) self.stub.send(parseXml(xml)) return d def test_nick(self): """ Send a nick change to the server. """ newNick = 'newNick' room = self._createRoom() def joined(room): self.assertEqual(self.roomJID, room.roomJID) self.assertEqual(newNick, room.nick) user = room.getUser(newNick) self.assertNotIdentical(None, user) self.assertEqual(newNick, user.nick) d = self.protocol.nick(self.roomJID, newNick) d.addCallback(joined) # Nick should not have been changed, yet, as we haven't gotten # confirmation, yet. self.assertEquals(self.nick, room.nick) # send back user presence, nick changed xml = u""" """ % (self.roomJID, newNick) self.stub.send(parseXml(xml)) return d def test_leave(self): """ Client leaves a room """ def joined(_): return self.protocol.leave(self.roomJID) def left(_): self.assertIdentical(None, self.protocol._getRoom(self.roomJID)) # Join the room d = self.protocol.join(self.roomJID, self.nick) d.addCallback(joined) d.addCallback(left) # Receive presence back from the room: joined. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) # Receive presence back from the room: left. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) return d def test_status(self): """ Change status """ def joined(_): d = self.protocol.status(self.roomJID, 'xa', 'testing MUC') d.addCallback(statusChanged) return d def statusChanged(room): self.assertEqual(self.roomJID, room.roomJID) user = room.getUser(self.nick) self.assertNotIdentical(None, user, 'User not found') self.assertEqual('testing MUC', user.status, 'Wrong status') self.assertEqual('xa', user.show, 'Wrong show') # Join the room d = self.protocol.join(self.roomJID, self.nick) d.addCallback(joined) # Receive presence back from the room: joined. xml = u""" """ % (self.userJID, self.occupantJID) self.stub.send(parseXml(xml)) # send back user presence, status changed xml = u""" xa testing MUC """ % self.occupantJID self.stub.send(parseXml(xml)) return d def test_destroy(self): """ Destroy a room. """ def destroyed(_): self.assertIdentical(None, self.protocol._getRoom(self.roomJID)) d = self.protocol.destroy(self.occupantJID, reason='Time to leave', alternate=JID('other@%s' % self.service), password='secret') d.addCallback(destroyed) iq = self.stub.output[-1] response = toResponse(iq, 'result') self.stub.send(response) return d wokkel-0.7.1/wokkel/test/test_disco.py0000775000175000017500000007562511734574117020626 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.disco}. """ from zope.interface import implements from twisted.internet import defer from twisted.trial import unittest from twisted.words.protocols.jabber.error import StanzaError from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import toResponse from twisted.words.xish import domish, utility from wokkel import data_form, disco from wokkel.generic import parseXml from wokkel.subprotocols import XMPPHandler from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info' NS_DISCO_ITEMS = 'http://jabber.org/protocol/disco#items' class DiscoFeatureTest(unittest.TestCase): """ Tests for L{disco.DiscoFeature}. """ def test_init(self): """ Test initialization with a with feature namespace URI. """ feature = disco.DiscoFeature(u'testns') self.assertEqual(u'testns', feature) def test_toElement(self): """ Test proper rendering to a DOM representation. The returned element should be properly named and have a C{var} attribute that holds the feature namespace URI. """ feature = disco.DiscoFeature(u'testns') element = feature.toElement() self.assertEqual(NS_DISCO_INFO, element.uri) self.assertEqual(u'feature', element.name) self.assertTrue(element.hasAttribute(u'var')) self.assertEqual(u'testns', element[u'var']) def test_fromElement(self): """ Test creating L{disco.DiscoFeature} from L{domish.Element}. """ element = domish.Element((NS_DISCO_INFO, u'feature')) element['var'] = u'testns' feature = disco.DiscoFeature.fromElement(element) self.assertEqual(u'testns', feature) class DiscoIdentityTest(unittest.TestCase): """ Tests for L{disco.DiscoIdentity}. """ def test_init(self): """ Test initialization with a category, type and name. """ identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom') self.assertEqual(u'conference', identity.category) self.assertEqual(u'text', identity.type) self.assertEqual(u'The chatroom', identity.name) def test_toElement(self): """ Test proper rendering to a DOM representation. The returned element should be properly named and have C{conference}, C{type}, and C{name} attributes. """ identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom') element = identity.toElement() self.assertEqual(NS_DISCO_INFO, element.uri) self.assertEqual(u'identity', element.name) self.assertEqual(u'conference', element.getAttribute(u'category')) self.assertEqual(u'text', element.getAttribute(u'type')) self.assertEqual(u'The chatroom', element.getAttribute(u'name')) def test_toElementWithoutName(self): """ Test proper rendering to a DOM representation without a name. The returned element should be properly named and have C{conference}, C{type} attributes, no C{name} attribute. """ identity = disco.DiscoIdentity(u'conference', u'text') element = identity.toElement() self.assertEqual(NS_DISCO_INFO, element.uri) self.assertEqual(u'identity', element.name) self.assertEqual(u'conference', element.getAttribute(u'category')) self.assertEqual(u'text', element.getAttribute(u'type')) self.assertFalse(element.hasAttribute(u'name')) def test_fromElement(self): """ Test creating L{disco.DiscoIdentity} from L{domish.Element}. """ element = domish.Element((NS_DISCO_INFO, u'identity')) element['category'] = u'conference' element['type'] = u'text' element['name'] = u'The chatroom' identity = disco.DiscoIdentity.fromElement(element) self.assertEqual(u'conference', identity.category) self.assertEqual(u'text', identity.type) self.assertEqual(u'The chatroom', identity.name) def test_fromElementWithoutName(self): """ Test creating L{disco.DiscoIdentity} from L{domish.Element}, no name. """ element = domish.Element((NS_DISCO_INFO, u'identity')) element['category'] = u'conference' element['type'] = u'text' identity = disco.DiscoIdentity.fromElement(element) self.assertEqual(u'conference', identity.category) self.assertEqual(u'text', identity.type) self.assertEqual(None, identity.name) class DiscoInfoTest(unittest.TestCase): """ Tests for L{disco.DiscoInfo}. """ def test_toElement(self): """ Test C{toElement} creates a correctly namespaced element, no node. """ info = disco.DiscoInfo() element = info.toElement() self.assertEqual(NS_DISCO_INFO, element.uri) self.assertEqual(u'query', element.name) self.assertFalse(element.hasAttribute(u'node')) def test_toElementNode(self): """ Test C{toElement} with a node. """ info = disco.DiscoInfo() info.nodeIdentifier = u'test' element = info.toElement() self.assertEqual(u'test', element.getAttribute(u'node')) def test_toElementChildren(self): """ Test C{toElement} creates a DOM with proper childs. """ info = disco.DiscoInfo() info.append(disco.DiscoFeature(u'jabber:iq:register')) info.append(disco.DiscoIdentity(u'conference', u'text')) info.append(data_form.Form(u'result')) element = info.toElement() featureElements = domish.generateElementsQNamed(element.children, u'feature', NS_DISCO_INFO) self.assertEqual(1, len(list(featureElements))) identityElements = domish.generateElementsQNamed(element.children, u'identity', NS_DISCO_INFO) self.assertEqual(1, len(list(identityElements))) extensionElements = domish.generateElementsQNamed(element.children, u'x', data_form.NS_X_DATA) self.assertEqual(1, len(list(extensionElements))) def test_fromElement(self): """ Test properties when creating L{disco.DiscoInfo} from L{domish.Element}. """ xml = """ http://jabber.org/protocol/muc#roominfo """ element = parseXml(xml) info = disco.DiscoInfo.fromElement(element) self.assertIn(u'http://jabber.org/protocol/muc', info.features) self.assertIn(u'jabber:iq:register', info.features) self.assertIn((u'conference', u'text'), info.identities) self.assertEqual(u'A Dark Cave', info.identities[(u'conference', u'text')]) self.assertIn(u'http://jabber.org/protocol/muc#roominfo', info.extensions) def test_fromElementItems(self): """ Test items when creating L{disco.DiscoInfo} from L{domish.Element}. """ xml = """ http://jabber.org/protocol/muc#roominfo """ element = parseXml(xml) info = disco.DiscoInfo.fromElement(element) info = list(info) self.assertEqual(4, len(info)) identity = info[0] self.assertEqual(u'conference', identity.category) self.assertEqual(u'http://jabber.org/protocol/muc', info[1]) self.assertEqual(u'jabber:iq:register', info[2]) extension = info[3] self.assertEqual(u'http://jabber.org/protocol/muc#roominfo', extension.formNamespace) def test_fromElementNoNode(self): """ Test creating L{disco.DiscoInfo} from L{domish.Element}, no node. """ xml = """""" element = parseXml(xml) info = disco.DiscoInfo.fromElement(element) self.assertEqual(u'', info.nodeIdentifier) def test_fromElementNode(self): """ Test creating L{disco.DiscoInfo} from L{domish.Element}, with node. """ xml = """ """ element = parseXml(xml) info = disco.DiscoInfo.fromElement(element) self.assertEqual(u'test', info.nodeIdentifier) class DiscoItemTest(unittest.TestCase): """ Tests for L{disco.DiscoItem}. """ def test_init(self): """ Test initialization with a category, type and name. """ item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node') self.assertEqual(JID(u'example.org'), item.entity) self.assertEqual(u'test', item.nodeIdentifier) self.assertEqual(u'The node', item.name) def test_toElement(self): """ Test proper rendering to a DOM representation. The returned element should be properly named and have C{jid}, C{node}, and C{name} attributes. """ item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node') element = item.toElement() self.assertEqual(NS_DISCO_ITEMS, element.uri) self.assertEqual(u'item', element.name) self.assertEqual(u'example.org', element.getAttribute(u'jid')) self.assertEqual(u'test', element.getAttribute(u'node')) self.assertEqual(u'The node', element.getAttribute(u'name')) def test_toElementWithoutName(self): """ Test proper rendering to a DOM representation without a name. The returned element should be properly named and have C{jid}, C{node} attributes, no C{name} attribute. """ item = disco.DiscoItem(JID(u'example.org'), u'test') element = item.toElement() self.assertEqual(NS_DISCO_ITEMS, element.uri) self.assertEqual(u'item', element.name) self.assertEqual(u'example.org', element.getAttribute(u'jid')) self.assertEqual(u'test', element.getAttribute(u'node')) self.assertFalse(element.hasAttribute(u'name')) def test_fromElement(self): """ Test creating L{disco.DiscoItem} from L{domish.Element}. """ element = domish.Element((NS_DISCO_ITEMS, u'item')) element[u'jid'] = u'example.org' element[u'node'] = u'test' element[u'name'] = u'The node' item = disco.DiscoItem.fromElement(element) self.assertEqual(JID(u'example.org'), item.entity) self.assertEqual(u'test', item.nodeIdentifier) self.assertEqual(u'The node', item.name) def test_fromElementNoNode(self): """ Test creating L{disco.DiscoItem} from L{domish.Element}, no node. """ element = domish.Element((NS_DISCO_ITEMS, u'item')) element[u'jid'] = u'example.org' element[u'name'] = u'The node' item = disco.DiscoItem.fromElement(element) self.assertEqual(JID(u'example.org'), item.entity) self.assertEqual(u'', item.nodeIdentifier) self.assertEqual(u'The node', item.name) def test_fromElementNoName(self): """ Test creating L{disco.DiscoItem} from L{domish.Element}, no name. """ element = domish.Element((NS_DISCO_ITEMS, u'item')) element[u'jid'] = u'example.org' element[u'node'] = u'test' item = disco.DiscoItem.fromElement(element) self.assertEqual(JID(u'example.org'), item.entity) self.assertEqual(u'test', item.nodeIdentifier) self.assertEqual(None, item.name) def test_fromElementBadJID(self): """ Test creating L{disco.DiscoItem} from L{domish.Element}, bad JID. """ element = domish.Element((NS_DISCO_ITEMS, u'item')) element[u'jid'] = u'ex@@@ample.org' item = disco.DiscoItem.fromElement(element) self.assertIdentical(None, item.entity) class DiscoItemsTest(unittest.TestCase): """ Tests for L{disco.DiscoItems}. """ def test_toElement(self): """ Test C{toElement} creates a correctly namespaced element, no node. """ items = disco.DiscoItems() element = items.toElement() self.assertEqual(NS_DISCO_ITEMS, element.uri) self.assertEqual(u'query', element.name) self.assertFalse(element.hasAttribute(u'node')) def test_toElementNode(self): """ Test C{toElement} with a node. """ items = disco.DiscoItems() items.nodeIdentifier = u'test' element = items.toElement() self.assertEqual(u'test', element.getAttribute(u'node')) def test_toElementChildren(self): """ Test C{toElement} creates a DOM with proper childs. """ items = disco.DiscoItems() items.append(disco.DiscoItem(JID(u'example.org'), u'test', u'A node')) element = items.toElement() itemElements = domish.generateElementsQNamed(element.children, u'item', NS_DISCO_ITEMS) self.assertEqual(1, len(list(itemElements))) def test_fromElement(self): """ Test creating L{disco.DiscoItems} from L{domish.Element}. """ xml = """ """ element = parseXml(xml) items = disco.DiscoItems.fromElement(element) items = list(items) self.assertEqual(1, len(items)) item = items[0] self.assertEqual(JID(u'example.org'), item.entity) self.assertEqual(u'test', item.nodeIdentifier) self.assertEqual(u'A node', item.name) def test_fromElementNoNode(self): """ Test creating L{disco.DiscoItems} from L{domish.Element}, no node. """ xml = """""" element = parseXml(xml) items = disco.DiscoItems.fromElement(element) self.assertEqual(u'', items.nodeIdentifier) def test_fromElementNode(self): """ Test creating L{disco.DiscoItems} from L{domish.Element}, with node. """ xml = """ """ element = parseXml(xml) items = disco.DiscoItems.fromElement(element) self.assertEqual(u'test', items.nodeIdentifier) class DiscoClientProtocolTest(unittest.TestCase): """ Tests for L{disco.DiscoClientProtocol}. """ def setUp(self): """ Set up stub and protocol for testing. """ self.stub = XmlStreamStub() self.patch(XMPPHandler, 'request', self.request) self.protocol = disco.DiscoClientProtocol() def request(self, request): element = request.toElement() self.stub.xmlstream.send(element) return defer.Deferred() def test_requestItems(self): """ Test request sent out by C{requestItems} and parsing of response. """ def cb(items): items = list(items) self.assertEqual(2, len(items)) self.assertEqual(JID(u'test.example.org'), items[0].entity) d = self.protocol.requestItems(JID(u'example.org'),u"foo") d.addCallback(cb) iq = self.stub.output[-1] self.assertEqual(u'example.org', iq.getAttribute(u'to')) self.assertEqual(u'get', iq.getAttribute(u'type')) self.assertEqual(u'foo', iq.query.getAttribute(u'node')) self.assertEqual(NS_DISCO_ITEMS, iq.query.uri) response = toResponse(iq, u'result') query = response.addElement((NS_DISCO_ITEMS, u'query')) element = query.addElement(u'item') element[u'jid'] = u'test.example.org' element[u'node'] = u'music' element[u'name'] = u'Music from the time of Shakespeare' element = query.addElement(u'item') element[u'jid'] = u"test2.example.org" d.callback(response) return d def test_requestItemsFrom(self): """ A disco items request can be sent with an explicit sender address. """ d = self.protocol.requestItems(JID(u'example.org'), sender=JID(u'test.example.org')) iq = self.stub.output[-1] self.assertEqual(u'test.example.org', iq.getAttribute(u'from')) response = toResponse(iq, u'result') response.addElement((NS_DISCO_ITEMS, u'query')) d.callback(response) return d def test_requestInfo(self): """ Test request sent out by C{requestInfo} and parsing of response. """ def cb(info): self.assertIn((u'conference', u'text'), info.identities) self.assertIn(u'http://jabber.org/protocol/disco#info', info.features) self.assertIn(u'http://jabber.org/protocol/muc', info.features) d = self.protocol.requestInfo(JID(u'example.org'),'foo') d.addCallback(cb) iq = self.stub.output[-1] self.assertEqual(u'example.org', iq.getAttribute(u'to')) self.assertEqual(u'get', iq.getAttribute(u'type')) self.assertEqual(u'foo', iq.query.getAttribute(u'node')) self.assertEqual(NS_DISCO_INFO, iq.query.uri) response = toResponse(iq, u'result') query = response.addElement((NS_DISCO_INFO, u'query')) element = query.addElement(u"identity") element[u'category'] = u'conference' # required element[u'type'] = u'text' # required element[u"name"] = u'Romeo and Juliet, Act II, Scene II' # optional element = query.addElement("feature") element[u'var'] = u'http://jabber.org/protocol/disco#info' # required element = query.addElement(u"feature") element[u'var'] = u'http://jabber.org/protocol/muc' d.callback(response) return d def test_requestInfoFrom(self): """ A disco info request can be sent with an explicit sender address. """ d = self.protocol.requestInfo(JID(u'example.org'), sender=JID(u'test.example.org')) iq = self.stub.output[-1] self.assertEqual(u'test.example.org', iq.getAttribute(u'from')) response = toResponse(iq, u'result') response.addElement((NS_DISCO_INFO, u'query')) d.callback(response) return d class DiscoHandlerTest(unittest.TestCase, TestableRequestHandlerMixin): """ Tests for L{disco.DiscoHandler}. """ def setUp(self): self.service = disco.DiscoHandler() def test_connectionInitializedObserveInfo(self): """ An observer for Disco Info requests is setup on stream initialization. """ xml = """ """ % NS_DISCO_INFO def handleRequest(iq): called.append(iq) called = [] self.service.xmlstream = utility.EventDispatcher() self.service.handleRequest = handleRequest self.service.connectionInitialized() self.service.xmlstream.dispatch(parseXml(xml)) self.assertEqual(1, len(called)) def test_connectionInitializedObserveItems(self): """ An observer for Disco Items requests is setup on stream initialization. """ xml = """ """ % NS_DISCO_ITEMS def handleRequest(iq): called.append(iq) called = [] self.service.xmlstream = utility.EventDispatcher() self.service.handleRequest = handleRequest self.service.connectionInitialized() self.service.xmlstream.dispatch(parseXml(xml)) self.assertEqual(1, len(called)) def test_onDiscoInfo(self): """ C{onDiscoInfo} should process an info request and return a response. The request should be parsed, C{info} called with the extracted parameters, and then the result should be formatted into a proper response element. """ xml = """ """ % NS_DISCO_INFO def cb(element): self.assertEqual('query', element.name) self.assertEqual(NS_DISCO_INFO, element.uri) self.assertEqual(NS_DISCO_INFO, element.identity.uri) self.assertEqual('dummy', element.identity['category']) self.assertEqual('generic', element.identity['type']) self.assertEqual('Generic Dummy Entity', element.identity['name']) self.assertEqual(NS_DISCO_INFO, element.feature.uri) self.assertEqual('jabber:iq:version', element.feature['var']) def info(requestor, target, nodeIdentifier): self.assertEqual(JID('test@example.com'), requestor) self.assertEqual(JID('example.com'), target) self.assertEqual('', nodeIdentifier) return defer.succeed([ disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), disco.DiscoFeature('jabber:iq:version') ]) self.service.info = info d = self.handleRequest(xml) d.addCallback(cb) return d def test_onDiscoInfoWithNoFromAttribute(self): """ Disco info request without a from attribute has requestor None. """ xml = """ """ % NS_DISCO_INFO def info(requestor, target, nodeIdentifier): self.assertEqual(None, requestor) return defer.succeed([ disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), disco.DiscoFeature('jabber:iq:version') ]) self.service.info = info d = self.handleRequest(xml) return d def test_onDiscoInfoWithNoToAttribute(self): """ Disco info request without a to attribute has target None. """ xml = """ """ % NS_DISCO_INFO def info(requestor, target, nodeIdentifier): self.assertEqual(JID('test@example.com'), requestor) return defer.succeed([ disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), disco.DiscoFeature('jabber:iq:version') ]) self.service.info = info d = self.handleRequest(xml) return d def test_onDiscoInfoWithNode(self): """ An info request for a node should return it in the response. """ xml = """ """ % NS_DISCO_INFO def cb(element): self.assertTrue(element.hasAttribute('node')) self.assertEqual('test', element['node']) def info(requestor, target, nodeIdentifier): self.assertEqual('test', nodeIdentifier) return defer.succeed([ disco.DiscoFeature('jabber:iq:version') ]) self.service.info = info d = self.handleRequest(xml) d.addCallback(cb) return d def test_onDiscoInfoWithNodeNoResults(self): """ An info request for a node with no results returns items-not-found. """ xml = """ """ % NS_DISCO_INFO def cb(exc): self.assertEquals('item-not-found', exc.condition) def info(requestor, target, nodeIdentifier): self.assertEqual('test', nodeIdentifier) return defer.succeed([]) self.service.info = info d = self.handleRequest(xml) self.assertFailure(d, StanzaError) d.addCallback(cb) return d def test_onDiscoItems(self): """ C{onDiscoItems} should process an items request and return a response. The request should be parsed, C{items} called with the extracted parameters, and then the result should be formatted into a proper response element. """ xml = """ """ % NS_DISCO_ITEMS def cb(element): self.assertEqual('query', element.name) self.assertEqual(NS_DISCO_ITEMS, element.uri) self.assertEqual(NS_DISCO_ITEMS, element.item.uri) self.assertEqual('example.com', element.item['jid']) self.assertEqual('test', element.item['node']) self.assertEqual('Test node', element.item['name']) def items(requestor, target, nodeIdentifier): self.assertEqual(JID('test@example.com'), requestor) self.assertEqual(JID('example.com'), target) self.assertEqual('', nodeIdentifier) return defer.succeed([ disco.DiscoItem(JID('example.com'), 'test', 'Test node'), ]) self.service.items = items d = self.handleRequest(xml) d.addCallback(cb) return d def test_onDiscoItemsWithNode(self): """ An items request for a node should return it in the response. """ xml = """ """ % NS_DISCO_ITEMS def cb(element): self.assertTrue(element.hasAttribute('node')) self.assertEqual('test', element['node']) def items(requestor, target, nodeIdentifier): self.assertEqual('test', nodeIdentifier) return defer.succeed([ disco.DiscoFeature('jabber:iq:version') ]) self.service.items = items d = self.handleRequest(xml) d.addCallback(cb) return d def test_info(self): """ C{info} should gather disco info from sibling handlers. """ discoItems = [disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), disco.DiscoFeature('jabber:iq:version') ] class DiscoResponder(XMPPHandler): implements(disco.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier): if not nodeIdentifier: return defer.succeed(discoItems) else: return defer.succeed([]) def cb(result): self.assertEquals(discoItems, result) self.service.parent = [self.service, DiscoResponder()] d = self.service.info(JID('test@example.com'), JID('example.com'), '') d.addCallback(cb) return d def test_infoNotDeferred(self): """ C{info} should gather disco info from sibling handlers. """ discoItems = [disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), disco.DiscoFeature('jabber:iq:version') ] class DiscoResponder(XMPPHandler): implements(disco.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier): if not nodeIdentifier: return discoItems else: return [] def cb(result): self.assertEquals(discoItems, result) self.service.parent = [self.service, DiscoResponder()] d = self.service.info(JID('test@example.com'), JID('example.com'), '') d.addCallback(cb) return d def test_items(self): """ C{info} should gather disco items from sibling handlers. """ discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')] class DiscoResponder(XMPPHandler): implements(disco.IDisco) def getDiscoItems(self, requestor, target, nodeIdentifier): if not nodeIdentifier: return defer.succeed(discoItems) else: return defer.succeed([]) def cb(result): self.assertEquals(discoItems, result) self.service.parent = [self.service, DiscoResponder()] d = self.service.items(JID('test@example.com'), JID('example.com'), '') d.addCallback(cb) return d def test_itemsNotDeferred(self): """ C{info} should also collect results not returned via a deferred. """ discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')] class DiscoResponder(XMPPHandler): implements(disco.IDisco) def getDiscoItems(self, requestor, target, nodeIdentifier): if not nodeIdentifier: return discoItems else: return [] def cb(result): self.assertEquals(discoItems, result) self.service.parent = [self.service, DiscoResponder()] d = self.service.items(JID('test@example.com'), JID('example.com'), '') d.addCallback(cb) return d wokkel-0.7.1/wokkel/test/test_client.py0000775000175000017500000001171512074266376020774 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.client}. """ from twisted.internet import defer from twisted.trial import unittest from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber.client import XMPPAuthenticator from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT from twisted.words.protocols.jabber.xmlstream import XMPPHandler from wokkel import client class XMPPClientTest(unittest.TestCase): """ Tests for L{client.XMPPClient}. """ def setUp(self): self.client = client.XMPPClient(JID('user@example.org'), 'secret') def test_jid(self): """ Make sure the JID we pass is stored on the client. """ self.assertEquals(JID('user@example.org'), self.client.jid) def test_jidWhenInitialized(self): """ Make sure that upon login, the JID is updated from the authenticator. """ xs = self.client.factory.buildProtocol(None) self.client.factory.authenticator.jid = JID('user@example.org/test') xs.dispatch(xs, xmlstream.STREAM_AUTHD_EVENT) self.assertEquals(JID('user@example.org/test'), self.client.jid) def test_domain(self): """ The domain to connect to is a byte string derived from the JID host. """ self.assertIsInstance(self.client.domain, bytes) self.assertEqual(b'example.org', self.client.domain) class DeferredClientFactoryTest(unittest.TestCase): """ Tests for L{client.DeferredClientFactory}. """ def setUp(self): self.factory = client.DeferredClientFactory(JID('user@example.org'), 'secret') def test_buildProtocol(self): """ The authenticator factory should be passed to its protocol and it should instantiate the authenticator and save it. L{xmlstream.XmlStream}s do that, but we also want to ensure it really is one. """ xs = self.factory.buildProtocol(None) self.assertIdentical(self.factory, xs.factory) self.assertIsInstance(xs, xmlstream.XmlStream) self.assertIsInstance(xs.authenticator, XMPPAuthenticator) def test_deferredOnInitialized(self): """ Test the factory's deferred on stream initialization. """ xs = self.factory.buildProtocol(None) xs.dispatch(xs, STREAM_AUTHD_EVENT) return self.factory.deferred def test_deferredOnNotInitialized(self): """ Test the factory's deferred on failed stream initialization. """ class TestException(Exception): pass xs = self.factory.buildProtocol(None) xs.dispatch(TestException(), INIT_FAILED_EVENT) self.assertFailure(self.factory.deferred, TestException) return self.factory.deferred def test_deferredOnConnectionFailure(self): """ Test the factory's deferred on connection faulure. """ class TestException(Exception): pass self.factory.buildProtocol(None) self.factory.clientConnectionFailed(self, TestException()) self.assertFailure(self.factory.deferred, TestException) return self.factory.deferred def test_addHandler(self): """ Test the addition of a protocol handler. """ handler = XMPPHandler() handler.setHandlerParent(self.factory.streamManager) self.assertIn(handler, self.factory.streamManager) self.assertIdentical(self.factory.streamManager, handler.parent) def test_removeHandler(self): """ Test removal of a protocol handler. """ handler = XMPPHandler() handler.setHandlerParent(self.factory.streamManager) handler.disownHandlerParent(self.factory.streamManager) self.assertNotIn(handler, self.factory.streamManager) self.assertIdentical(None, handler.parent) class ClientCreatorTest(unittest.TestCase): """ Tests for L{client.clientCreator}. """ def test_call(self): """ The factory is passed to an SRVConnector and a connection initiated. """ d1 = defer.Deferred() factory = client.DeferredClientFactory(JID('user@example.org'), 'secret') def cb(connector): self.assertEqual('xmpp-client', connector.service) self.assertIsInstance(connector.domain, bytes) self.assertEqual(b'example.org', connector.domain) self.assertEqual(factory, connector.factory) def connect(connector): d1.callback(connector) d1.addCallback(cb) self.patch(client.SRVConnector, 'connect', connect) d2 = client.clientCreator(factory) self.assertEqual(factory.deferred, d2) return d1 wokkel-0.7.1/wokkel/test/test_pubsub.py0000775000175000017500000042657212014253400021003 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.pubsub} """ from zope.interface import verify from twisted.trial import unittest from twisted.internet import defer from twisted.words.xish import domish from twisted.words.protocols.jabber import error from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import toResponse from wokkel import data_form, disco, iwokkel, pubsub, shim from wokkel.generic import parseXml from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub NS_PUBSUB = 'http://jabber.org/protocol/pubsub' NS_PUBSUB_NODE_CONFIG = 'http://jabber.org/protocol/pubsub#node_config' NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors' NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data' NS_PUBSUB_SUBSCRIBE_OPTIONS = 'http://jabber.org/protocol/pubsub#subscribe_options' def calledAsync(fn): """ Function wrapper that fires a deferred upon calling the given function. """ d = defer.Deferred() def func(*args, **kwargs): try: result = fn(*args, **kwargs) except: d.errback() else: d.callback(result) return d, func class SubscriptionTest(unittest.TestCase): """ Tests for L{pubsub.Subscription}. """ def test_fromElement(self): """ fromElement parses a subscription from XML DOM. """ xml = """ """ subscription = pubsub.Subscription.fromElement(parseXml(xml)) self.assertEqual('test', subscription.nodeIdentifier) self.assertEqual(JID('user@example.org/Home'), subscription.subscriber) self.assertEqual('pending', subscription.state) self.assertIdentical(None, subscription.subscriptionIdentifier) def test_fromElementWithSubscriptionIdentifier(self): """ A subscription identifier in the subscription should be parsed, too. """ xml = """ """ subscription = pubsub.Subscription.fromElement(parseXml(xml)) self.assertEqual('1234', subscription.subscriptionIdentifier) def test_toElement(self): """ Rendering a Subscription should yield the proper attributes. """ subscription = pubsub.Subscription('test', JID('user@example.org/Home'), 'pending') element = subscription.toElement() self.assertEqual('subscription', element.name) self.assertEqual(None, element.uri) self.assertEqual('test', element.getAttribute('node')) self.assertEqual('user@example.org/Home', element.getAttribute('jid')) self.assertEqual('pending', element.getAttribute('subscription')) self.assertFalse(element.hasAttribute('subid')) def test_toElementEmptyNodeIdentifier(self): """ The empty node identifier should not yield a node attribute. """ subscription = pubsub.Subscription('', JID('user@example.org/Home'), 'pending') element = subscription.toElement() self.assertFalse(element.hasAttribute('node')) def test_toElementWithSubscriptionIdentifier(self): """ The subscription identifier, if set, is in the subid attribute. """ subscription = pubsub.Subscription('test', JID('user@example.org/Home'), 'pending', subscriptionIdentifier='1234') element = subscription.toElement() self.assertEqual('1234', element.getAttribute('subid')) class PubSubClientTest(unittest.TestCase): timeout = 2 def setUp(self): self.stub = XmlStreamStub() self.protocol = pubsub.PubSubClient() self.protocol.xmlstream = self.stub.xmlstream self.protocol.connectionInitialized() def test_interface(self): """ Do instances of L{pubsub.PubSubClient} provide L{iwokkel.IPubSubClient}? """ verify.verifyObject(iwokkel.IPubSubClient, self.protocol) def test_eventItems(self): """ Test receiving an items event resulting in a call to itemsReceived. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' event = message.addElement((NS_PUBSUB_EVENT, 'event')) items = event.addElement('items') items['node'] = 'test' item1 = items.addElement('item') item1['id'] = 'item1' item2 = items.addElement('retract') item2['id'] = 'item2' item3 = items.addElement('item') item3['id'] = 'item3' def itemsReceived(event): self.assertEquals(JID('user@example.org/home'), event.recipient) self.assertEquals(JID('pubsub.example.org'), event.sender) self.assertEquals('test', event.nodeIdentifier) self.assertEquals([item1, item2, item3], event.items) d, self.protocol.itemsReceived = calledAsync(itemsReceived) self.stub.send(message) return d def test_eventItemsCollection(self): """ Test receiving an items event resulting in a call to itemsReceived. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' event = message.addElement((NS_PUBSUB_EVENT, 'event')) items = event.addElement('items') items['node'] = 'test' headers = shim.Headers([('Collection', 'collection')]) message.addChild(headers) def itemsReceived(event): self.assertEquals(JID('user@example.org/home'), event.recipient) self.assertEquals(JID('pubsub.example.org'), event.sender) self.assertEquals('test', event.nodeIdentifier) self.assertEquals({'Collection': ['collection']}, event.headers) d, self.protocol.itemsReceived = calledAsync(itemsReceived) self.stub.send(message) return d def test_eventItemsError(self): """ An error message with embedded event should not be handled. This test uses an items event, which should not result in itemsReceived being called. In general message.handled should be False. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' message['type'] = 'error' event = message.addElement((NS_PUBSUB_EVENT, 'event')) items = event.addElement('items') items['node'] = 'test' class UnexpectedCall(Exception): pass def itemsReceived(event): raise UnexpectedCall("Unexpected call to itemsReceived") self.protocol.itemsReceived = itemsReceived self.stub.send(message) self.assertFalse(message.handled) def test_eventDelete(self): """ Test receiving a delete event resulting in a call to deleteReceived. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' event = message.addElement((NS_PUBSUB_EVENT, 'event')) delete = event.addElement('delete') delete['node'] = 'test' def deleteReceived(event): self.assertEquals(JID('user@example.org/home'), event.recipient) self.assertEquals(JID('pubsub.example.org'), event.sender) self.assertEquals('test', event.nodeIdentifier) d, self.protocol.deleteReceived = calledAsync(deleteReceived) self.stub.send(message) return d def test_eventDeleteRedirect(self): """ Test receiving a delete event with a redirect URI. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' event = message.addElement((NS_PUBSUB_EVENT, 'event')) delete = event.addElement('delete') delete['node'] = 'test' uri = 'xmpp:pubsub.example.org?;node=test2' delete.addElement('redirect')['uri'] = uri def deleteReceived(event): self.assertEquals(JID('user@example.org/home'), event.recipient) self.assertEquals(JID('pubsub.example.org'), event.sender) self.assertEquals('test', event.nodeIdentifier) self.assertEquals(uri, event.redirectURI) d, self.protocol.deleteReceived = calledAsync(deleteReceived) self.stub.send(message) return d def test_event_purge(self): """ Test receiving a purge event resulting in a call to purgeReceived. """ message = domish.Element((None, 'message')) message['from'] = 'pubsub.example.org' message['to'] = 'user@example.org/home' event = message.addElement((NS_PUBSUB_EVENT, 'event')) items = event.addElement('purge') items['node'] = 'test' def purgeReceived(event): self.assertEquals(JID('user@example.org/home'), event.recipient) self.assertEquals(JID('pubsub.example.org'), event.sender) self.assertEquals('test', event.nodeIdentifier) d, self.protocol.purgeReceived = calledAsync(purgeReceived) self.stub.send(message) return d def test_createNode(self): """ Test sending create request. """ def cb(nodeIdentifier): self.assertEquals('test', nodeIdentifier) d = self.protocol.createNode(JID('pubsub.example.org'), 'test') d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'create', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_createNodeInstant(self): """ Test sending create request resulting in an instant node. """ def cb(nodeIdentifier): self.assertEquals('test', nodeIdentifier) d = self.protocol.createNode(JID('pubsub.example.org')) d.addCallback(cb) iq = self.stub.output[-1] children = list(domish.generateElementsQNamed(iq.pubsub.children, 'create', NS_PUBSUB)) child = children[0] self.assertFalse(child.hasAttribute('node')) response = toResponse(iq, 'result') command = response.addElement((NS_PUBSUB, 'pubsub')) create = command.addElement('create') create['node'] = 'test' self.stub.send(response) return d def test_createNodeRenamed(self): """ Test sending create request resulting in renamed node. """ def cb(nodeIdentifier): self.assertEquals('test2', nodeIdentifier) d = self.protocol.createNode(JID('pubsub.example.org'), 'test') d.addCallback(cb) iq = self.stub.output[-1] children = list(domish.generateElementsQNamed(iq.pubsub.children, 'create', NS_PUBSUB)) child = children[0] self.assertEquals('test', child['node']) response = toResponse(iq, 'result') command = response.addElement((NS_PUBSUB, 'pubsub')) create = command.addElement('create') create['node'] = 'test2' self.stub.send(response) return d def test_createNodeWithSender(self): """ Test sending create request from a specific JID. """ d = self.protocol.createNode(JID('pubsub.example.org'), 'test', sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_createNodeWithConfig(self): """ Test sending create request with configuration options """ options = { 'pubsub#title': 'Princely Musings (Atom)', 'pubsub#deliver_payloads': True, 'pubsub#persist_items': '1', 'pubsub#max_items': '10', 'pubsub#access_model': 'open', 'pubsub#type': 'http://www.w3.org/2005/Atom', } d = self.protocol.createNode(JID('pubsub.example.org'), 'test', sender=JID('user@example.org'), options=options) iq = self.stub.output[-1] # check if there is exactly one configure element children = list(domish.generateElementsQNamed(iq.pubsub.children, 'configure', NS_PUBSUB)) self.assertEqual(1, len(children)) # check that it has a configuration form form = data_form.findForm(children[0], NS_PUBSUB_NODE_CONFIG) self.assertEqual('submit', form.formType) response = toResponse(iq, 'result') self.stub.send(response) return d def test_deleteNode(self): """ Test sending delete request. """ d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test') iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'delete', NS_PUBSUB_OWNER)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_deleteNodeWithSender(self): """ Test sending delete request. """ d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test', sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_publish(self): """ Test sending publish request. """ item = pubsub.Item() d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item]) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'publish', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) items = list(domish.generateElementsQNamed(child.children, 'item', NS_PUBSUB)) self.assertEquals(1, len(items)) self.assertIdentical(item, items[0]) response = toResponse(iq, 'result') self.stub.send(response) return d def test_publishNoItems(self): """ Test sending publish request without items. """ d = self.protocol.publish(JID('pubsub.example.org'), 'test') iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'publish', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_publishWithSender(self): """ Test sending publish request from a specific JID. """ item = pubsub.Item() d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item], JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) response = toResponse(iq, 'result') self.stub.send(response) return d def test_subscribe(self): """ Test sending subscription request. """ d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'subscribe', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) self.assertEquals('user@example.org', child['jid']) response = toResponse(iq, 'result') pubsub = response.addElement((NS_PUBSUB, 'pubsub')) subscription = pubsub.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'subscribed' self.stub.send(response) return d def test_subscribeReturnsSubscription(self): """ A successful subscription should return a Subscription instance. """ def cb(subscription): self.assertEqual(JID('user@example.org'), subscription.subscriber) d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) d.addCallback(cb) iq = self.stub.output[-1] response = toResponse(iq, 'result') pubsub = response.addElement((NS_PUBSUB, 'pubsub')) subscription = pubsub.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'subscribed' self.stub.send(response) return d def test_subscribePending(self): """ Test sending subscription request that results in a pending subscription. """ d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) iq = self.stub.output[-1] response = toResponse(iq, 'result') command = response.addElement((NS_PUBSUB, 'pubsub')) subscription = command.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'pending' self.stub.send(response) self.assertFailure(d, pubsub.SubscriptionPending) return d def test_subscribeUnconfigured(self): """ Test sending subscription request that results in an unconfigured subscription. """ d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) iq = self.stub.output[-1] response = toResponse(iq, 'result') command = response.addElement((NS_PUBSUB, 'pubsub')) subscription = command.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'unconfigured' self.stub.send(response) self.assertFailure(d, pubsub.SubscriptionUnconfigured) return d def test_subscribeWithOptions(self): options = {'pubsub#deliver': False} d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org'), options=options) iq = self.stub.output[-1] # Check options present childNames = [] for element in iq.pubsub.elements(): if element.uri == NS_PUBSUB: childNames.append(element.name) self.assertEqual(['subscribe', 'options'], childNames) form = data_form.findForm(iq.pubsub.options, NS_PUBSUB_SUBSCRIBE_OPTIONS) self.assertEqual('submit', form.formType) form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) self.assertEqual(options, form.getValues()) # Send response response = toResponse(iq, 'result') pubsub = response.addElement((NS_PUBSUB, 'pubsub')) subscription = pubsub.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'subscribed' self.stub.send(response) return d def test_subscribeWithSender(self): """ Test sending subscription request from a specific JID. """ d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org'), sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) response = toResponse(iq, 'result') pubsub = response.addElement((NS_PUBSUB, 'pubsub')) subscription = pubsub.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'subscribed' self.stub.send(response) return d def test_subscribeReturningSubscriptionIdentifier(self): """ Test sending subscription request with subscription identifier. """ def cb(subscription): self.assertEqual('1234', subscription.subscriptionIdentifier) d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) d.addCallback(cb) iq = self.stub.output[-1] response = toResponse(iq, 'result') pubsub = response.addElement((NS_PUBSUB, 'pubsub')) subscription = pubsub.addElement('subscription') subscription['node'] = 'test' subscription['jid'] = 'user@example.org' subscription['subscription'] = 'subscribed' subscription['subid'] = '1234' self.stub.send(response) return d def test_unsubscribe(self): """ Test sending unsubscription request. """ d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('set', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'unsubscribe', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) self.assertEquals('user@example.org', child['jid']) self.stub.send(toResponse(iq, 'result')) return d def test_unsubscribeWithSender(self): """ Test sending unsubscription request from a specific JID. """ d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', JID('user@example.org'), sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) self.stub.send(toResponse(iq, 'result')) return d def test_unsubscribeWithSubscriptionIdentifier(self): """ Test sending unsubscription request with subscription identifier. """ d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', JID('user@example.org'), subscriptionIdentifier='1234') iq = self.stub.output[-1] child = iq.pubsub.unsubscribe self.assertEquals('1234', child['subid']) self.stub.send(toResponse(iq, 'result')) return d def test_items(self): """ Test sending items request. """ def cb(items): self.assertEquals([], items) d = self.protocol.items(JID('pubsub.example.org'), 'test') d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('get', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'items', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) response = toResponse(iq, 'result') items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') items['node'] = 'test' self.stub.send(response) return d def test_itemsMaxItems(self): """ Test sending items request, with limit on the number of items. """ def cb(items): self.assertEquals(2, len(items)) self.assertEquals([item1, item2], items) d = self.protocol.items(JID('pubsub.example.org'), 'test', maxItems=2) d.addCallback(cb) iq = self.stub.output[-1] self.assertEquals('pubsub.example.org', iq.getAttribute('to')) self.assertEquals('get', iq.getAttribute('type')) self.assertEquals('pubsub', iq.pubsub.name) self.assertEquals(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'items', NS_PUBSUB)) self.assertEquals(1, len(children)) child = children[0] self.assertEquals('test', child['node']) self.assertEquals('2', child['max_items']) response = toResponse(iq, 'result') items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') items['node'] = 'test' item1 = items.addElement('item') item1['id'] = 'item1' item2 = items.addElement('item') item2['id'] = 'item2' self.stub.send(response) return d def test_itemsWithSubscriptionIdentifier(self): """ Test sending items request with a subscription identifier. """ d = self.protocol.items(JID('pubsub.example.org'), 'test', subscriptionIdentifier='1234') iq = self.stub.output[-1] child = iq.pubsub.items self.assertEquals('1234', child['subid']) response = toResponse(iq, 'result') items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') items['node'] = 'test' self.stub.send(response) return d def test_itemsWithSender(self): """ Test sending items request from a specific JID. """ d = self.protocol.items(JID('pubsub.example.org'), 'test', sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEquals('user@example.org', iq['from']) response = toResponse(iq, 'result') items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') items['node'] = 'test' self.stub.send(response) return d def test_getOptions(self): def cb(form): self.assertEqual('form', form.formType) self.assertEqual(NS_PUBSUB_SUBSCRIBE_OPTIONS, form.formNamespace) field = form.fields['pubsub#deliver'] self.assertEqual('boolean', field.fieldType) self.assertIdentical(True, field.value) self.assertEqual('Enable delivery?', field.label) d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', JID('user@example.org'), sender=JID('user@example.org')) d.addCallback(cb) iq = self.stub.output[-1] self.assertEqual('pubsub.example.org', iq.getAttribute('to')) self.assertEqual('get', iq.getAttribute('type')) self.assertEqual('pubsub', iq.pubsub.name) self.assertEqual(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'options', NS_PUBSUB)) self.assertEqual(1, len(children)) child = children[0] self.assertEqual('test', child['node']) self.assertEqual(0, len(child.children)) # Send response form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) form.addField(data_form.Field('boolean', var='pubsub#deliver', label='Enable delivery?', value=True)) response = toResponse(iq, 'result') response.addElement((NS_PUBSUB, 'pubsub')) response.pubsub.addElement('options') response.pubsub.options.addChild(form.toElement()) self.stub.send(response) return d def test_getOptionsWithSubscriptionIdentifier(self): """ Getting options with a subid should have the subid in the request. """ d = self.protocol.getOptions(JID('pubsub.example.org'), 'test', JID('user@example.org'), sender=JID('user@example.org'), subscriptionIdentifier='1234') iq = self.stub.output[-1] child = iq.pubsub.options self.assertEqual('1234', child['subid']) # Send response form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) form.addField(data_form.Field('boolean', var='pubsub#deliver', label='Enable delivery?', value=True)) response = toResponse(iq, 'result') response.addElement((NS_PUBSUB, 'pubsub')) response.pubsub.addElement('options') response.pubsub.options.addChild(form.toElement()) self.stub.send(response) return d def test_setOptions(self): """ setOptions should send out a options-set request. """ options = {'pubsub#deliver': False} d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', JID('user@example.org'), options, sender=JID('user@example.org')) iq = self.stub.output[-1] self.assertEqual('pubsub.example.org', iq.getAttribute('to')) self.assertEqual('set', iq.getAttribute('type')) self.assertEqual('pubsub', iq.pubsub.name) self.assertEqual(NS_PUBSUB, iq.pubsub.uri) children = list(domish.generateElementsQNamed(iq.pubsub.children, 'options', NS_PUBSUB)) self.assertEqual(1, len(children)) child = children[0] self.assertEqual('test', child['node']) form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) self.assertEqual('submit', form.formType) form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) self.assertEqual(options, form.getValues()) response = toResponse(iq, 'result') self.stub.send(response) return d def test_setOptionsWithSubscriptionIdentifier(self): """ setOptions should send out a options-set request with subid. """ options = {'pubsub#deliver': False} d = self.protocol.setOptions(JID('pubsub.example.org'), 'test', JID('user@example.org'), options, subscriptionIdentifier='1234', sender=JID('user@example.org')) iq = self.stub.output[-1] child = iq.pubsub.options self.assertEqual('1234', child['subid']) form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS) self.assertEqual('submit', form.formType) form.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) self.assertEqual(options, form.getValues()) response = toResponse(iq, 'result') self.stub.send(response) return d class PubSubRequestTest(unittest.TestCase): def test_fromElementUnknown(self): """ An unknown verb raises NotImplementedError. """ xml = """ """ self.assertRaises(NotImplementedError, pubsub.PubSubRequest.fromElement, parseXml(xml)) def test_fromElementKnownBadCombination(self): """ Multiple verbs in an unknown configuration raises NotImplementedError. """ xml = """ """ self.assertRaises(NotImplementedError, pubsub.PubSubRequest.fromElement, parseXml(xml)) def test_fromElementPublish(self): """ Test parsing a publish request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('publish', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual([], request.items) def test_fromElementPublishItems(self): """ Test parsing a publish request with items. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual(2, len(request.items)) self.assertEqual(u'item1', request.items[0]["id"]) self.assertEqual(u'item2', request.items[1]["id"]) def test_fromElementPublishItemsOptions(self): """ Test parsing a publish request with items and options. Note that publishing options are not supported, but passing them shouldn't affect processing of the publish request itself. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual(2, len(request.items)) self.assertEqual(u'item1', request.items[0]["id"]) self.assertEqual(u'item2', request.items[1]["id"]) def test_fromElementPublishNoNode(self): """ A publish request to the root node should raise an exception. """ xml = """ """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) self.assertEqual('nodeid-required', err.appCondition.name) def test_fromElementSubscribe(self): """ Test parsing a subscription request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('subscribe', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual(JID('user@example.org/Home'), request.subscriber) def test_fromElementSubscribeEmptyNode(self): """ Test parsing a subscription request to the root node. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('', request.nodeIdentifier) def test_fromElementSubscribeNoJID(self): """ Subscribe requests without a JID should raise a bad-request exception. """ xml = """ """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) self.assertEqual('jid-required', err.appCondition.name) def test_fromElementSubscribeWithOptions(self): """ Test parsing a subscription request. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('subscribe', request.verb) request.options.typeCheck({'pubsub#deliver': {'type': 'boolean'}}) self.assertEqual({'pubsub#deliver': True}, request.options.getValues()) def test_fromElementSubscribeWithOptionsBadFormType(self): """ The options form should have the right type. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual("Unexpected form type 'result'", err.text) self.assertEqual(None, err.appCondition) def test_fromElementSubscribeWithOptionsEmpty(self): """ When no (suitable) form is found, the options are empty. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('subscribe', request.verb) self.assertEqual({}, request.options.getValues()) def test_fromElementUnsubscribe(self): """ Test parsing an unsubscription request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('unsubscribe', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual(JID('user@example.org/Home'), request.subscriber) def test_fromElementUnsubscribeWithSubscriptionIdentifier(self): """ Test parsing an unsubscription request with subscription identifier. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('1234', request.subscriptionIdentifier) def test_fromElementUnsubscribeNoJID(self): """ Unsubscribe requests without a JID should raise a bad-request exception. """ xml = """ """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) self.assertEqual('jid-required', err.appCondition.name) def test_fromElementOptionsGet(self): """ Test parsing a request for getting subscription options. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('optionsGet', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual(JID('user@example.org/Home'), request.subscriber) def test_fromElementOptionsGetWithSubscriptionIdentifier(self): """ Test parsing a request for getting subscription options with subid. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('1234', request.subscriptionIdentifier) def test_fromElementOptionsSet(self): """ Test parsing a request for setting subscription options. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('optionsSet', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual(JID('user@example.org/Home'), request.subscriber) self.assertEqual({'pubsub#deliver': '1'}, request.options.getValues()) def test_fromElementOptionsSetWithSubscriptionIdentifier(self): """ Test parsing a request for setting subscription options with subid. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('1234', request.subscriptionIdentifier) def test_fromElementOptionsSetCancel(self): """ Test parsing a request for cancelling setting subscription options. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('cancel', request.options.formType) def test_fromElementOptionsSetBadFormType(self): """ On a options set request unknown fields should be ignored. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual("Unexpected form type 'result'", err.text) self.assertEqual(None, err.appCondition) def test_fromElementOptionsSetNoForm(self): """ On a options set request a form is required. """ xml = """ """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual(None, err.appCondition) def test_fromElementSubscriptions(self): """ Test parsing a request for all subscriptions. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('subscriptions', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) def test_fromElementAffiliations(self): """ Test parsing a request for all affiliations. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('affiliations', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) def test_fromElementCreate(self): """ Test parsing a request to create a node. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('create', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('mynode', request.nodeIdentifier) self.assertIdentical(None, request.options) def test_fromElementCreateInstant(self): """ Test parsing a request to create an instant node. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertIdentical(None, request.nodeIdentifier) def test_fromElementCreateConfigureEmpty(self): """ Test parsing a request to create a node with an empty configuration. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual({}, request.options.getValues()) self.assertEqual(u'mynode', request.nodeIdentifier) def test_fromElementCreateConfigureEmptyWrongOrder(self): """ Test parsing a request to create a node and configure, wrong order. The C{configure} element should come after the C{create} request, but we should accept both orders. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual({}, request.options.getValues()) self.assertEqual(u'mynode', request.nodeIdentifier) def test_fromElementCreateConfigure(self): """ Test parsing a request to create a node. """ xml = """ http://jabber.org/protocol/pubsub#node_config open 0 """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) values = request.options self.assertIn('pubsub#access_model', values) self.assertEqual(u'open', values['pubsub#access_model']) self.assertIn('pubsub#persist_items', values) self.assertEqual(u'0', values['pubsub#persist_items']) def test_fromElementCreateConfigureBadFormType(self): """ The form of a node creation request should have the right type. """ xml = """ http://jabber.org/protocol/pubsub#node_config open 0 """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual("Unexpected form type 'result'", err.text) self.assertEqual(None, err.appCondition) def test_fromElementDefault(self): """ Parsing default node configuration request sets required attributes. Besides C{verb}, C{sender} and C{recipient}, we expect C{nodeType} to be set. If not passed it receives the default C{u'leaf'}. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEquals(u'default', request.verb) self.assertEquals(JID('user@example.org'), request.sender) self.assertEquals(JID('pubsub.example.org'), request.recipient) self.assertEquals(u'leaf', request.nodeType) def test_fromElementDefaultCollection(self): """ Parsing default request for collection sets nodeType to collection. """ xml = """ http://jabber.org/protocol/pubsub#node_config collection """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEquals('collection', request.nodeType) def test_fromElementConfigureGet(self): """ Test parsing a node configuration get request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('configureGet', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) def test_fromElementConfigureSet(self): """ On a node configuration set request the Data Form is parsed. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('configureSet', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual({'pubsub#deliver_payloads': '0', 'pubsub#persist_items': '1'}, request.options.getValues()) def test_fromElementConfigureSetCancel(self): """ The node configuration is cancelled, so no options. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('cancel', request.options.formType) def test_fromElementConfigureSetBadFormType(self): """ The form of a node configuraton set request should have the right type. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual("Unexpected form type 'result'", err.text) self.assertEqual(None, err.appCondition) def test_fromElementConfigureSetNoForm(self): """ On a node configuration set request a form is required. """ xml = """ """ err = self.assertRaises(error.StanzaError, pubsub.PubSubRequest.fromElement, parseXml(xml)) self.assertEqual('bad-request', err.condition) self.assertEqual(None, err.appCondition) def test_fromElementItems(self): """ Test parsing an items request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('items', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertIdentical(None, request.maxItems) self.assertIdentical(None, request.subscriptionIdentifier) self.assertEqual([], request.itemIdentifiers) def test_fromElementItemsSubscriptionIdentifier(self): """ Test parsing an items request with subscription identifier. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('1234', request.subscriptionIdentifier) def test_fromElementRetract(self): """ Test parsing a retract request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('retract', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) self.assertEqual(['item1', 'item2'], request.itemIdentifiers) def test_fromElementPurge(self): """ Test parsing a purge request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('purge', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) def test_fromElementDelete(self): """ Test parsing a delete request. """ xml = """ """ request = pubsub.PubSubRequest.fromElement(parseXml(xml)) self.assertEqual('delete', request.verb) self.assertEqual(JID('user@example.org'), request.sender) self.assertEqual(JID('pubsub.example.org'), request.recipient) self.assertEqual('test', request.nodeIdentifier) class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin): """ Tests for L{pubsub.PubSubService}. """ def setUp(self): self.stub = XmlStreamStub() self.resource = pubsub.PubSubResource() self.service = pubsub.PubSubService(self.resource) self.service.send = self.stub.xmlstream.send def test_interface(self): """ Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}? """ verify.verifyObject(iwokkel.IPubSubService, self.service) def test_interfaceIDisco(self): """ Do instances of L{pubsub.PubSubService} provide L{iwokkel.IDisco}? """ verify.verifyObject(iwokkel.IDisco, self.service) def test_connectionMade(self): """ Verify setup of observers in L{pubsub.connectionMade}. """ requests = [] def handleRequest(iq): requests.append(iq) self.service.xmlstream = self.stub.xmlstream self.service.handleRequest = handleRequest self.service.connectionMade() for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER): for stanzaType in ('get', 'set'): iq = domish.Element((None, 'iq')) iq['type'] = stanzaType iq.addElement((namespace, 'pubsub')) self.stub.xmlstream.dispatch(iq) self.assertEqual(4, len(requests)) def test_getDiscoInfo(self): """ Test getDiscoInfo calls getNodeInfo and returns some minimal info. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn(('pubsub', 'service'), discoInfo.identities) self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoInfoNodeType(self): """ Test getDiscoInfo with node type. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn(('pubsub', 'collection'), discoInfo.identities) def getInfo(requestor, target, nodeIdentifier): return defer.succeed({'type': 'collection', 'meta-data': {}}) self.resource.getInfo = getInfo d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoInfoMetaData(self): """ Test getDiscoInfo with returned meta data. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn(('pubsub', 'leaf'), discoInfo.identities) self.assertIn(NS_PUBSUB_META_DATA, discoInfo.extensions) form = discoInfo.extensions[NS_PUBSUB_META_DATA] self.assertIn('pubsub#node_type', form.fields) def getInfo(requestor, target, nodeIdentifier): metaData = [{'var': 'pubsub#persist_items', 'label': 'Persist items to storage', 'value': True}] return defer.succeed({'type': 'leaf', 'meta-data': metaData}) self.resource.getInfo = getInfo d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoInfoResourceFeatures(self): """ Test getDiscoInfo with the resource features. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn('http://jabber.org/protocol/pubsub#publish', discoInfo.features) self.resource.features = ['publish'] d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoInfoBadResponse(self): """ If getInfo returns invalid response, it should be logged, then ignored. """ def cb(info): self.assertEquals([], info) self.assertEqual(1, len(self.flushLoggedErrors(TypeError))) def getInfo(requestor, target, nodeIdentifier): return defer.succeed('bad response') self.resource.getInfo = getInfo d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), 'test') d.addCallback(cb) return d def test_getDiscoInfoException(self): """ If getInfo returns invalid response, it should be logged, then ignored. """ def cb(info): self.assertEquals([], info) self.assertEqual(1, len(self.flushLoggedErrors(NotImplementedError))) def getInfo(requestor, target, nodeIdentifier): return defer.fail(NotImplementedError()) self.resource.getInfo = getInfo d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), 'test') d.addCallback(cb) return d def test_getDiscoItemsRoot(self): """ Test getDiscoItems on the root node. """ def getNodes(requestor, service, nodeIdentifier): return defer.succeed(['node1', 'node2']) def cb(items): self.assertEqual(2, len(items)) item1, item2 = items self.assertEqual(JID('pubsub.example.org'), item1.entity) self.assertEqual('node1', item1.nodeIdentifier) self.assertEqual(JID('pubsub.example.org'), item2.entity) self.assertEqual('node2', item2.nodeIdentifier) self.resource.getNodes = getNodes d = self.service.getDiscoItems(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoItemsRootHideNodes(self): """ Test getDiscoItems on the root node. """ def getNodes(requestor, service, nodeIdentifier): raise Exception("Unexpected call to getNodes") def cb(items): self.assertEqual([], items) self.service.hideNodes = True self.resource.getNodes = getNodes d = self.service.getDiscoItems(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoItemsNonRoot(self): """ Test getDiscoItems on a non-root node. """ def getNodes(requestor, service, nodeIdentifier): return defer.succeed(['node1', 'node2']) def cb(items): self.assertEqual(2, len(items)) self.resource.getNodes = getNodes d = self.service.getDiscoItems(JID('user@example.org/home'), JID('pubsub.example.org'), 'test') d.addCallback(cb) return d def test_on_publish(self): """ A publish request should result in L{PubSubService.publish} being called. """ xml = """ """ def publish(request): return defer.succeed(None) self.resource.publish = publish verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_subscribe(self): """ A successful subscription should return the current subscription. """ xml = """ """ def subscribe(request): return defer.succeed(pubsub.Subscription(request.nodeIdentifier, request.subscriber, 'subscribed')) def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB, element.uri) subscription = element.subscription self.assertEqual(NS_PUBSUB, subscription.uri) self.assertEqual('test', subscription['node']) self.assertEqual('user@example.org/Home', subscription['jid']) self.assertEqual('subscribed', subscription['subscription']) self.resource.subscribe = subscribe verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_subscribeEmptyNode(self): """ A successful subscription on root node should return no node attribute. """ xml = """ """ def subscribe(request): return defer.succeed(pubsub.Subscription(request.nodeIdentifier, request.subscriber, 'subscribed')) def cb(element): self.assertFalse(element.subscription.hasAttribute('node')) self.resource.subscribe = subscribe verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_subscribeSubscriptionIdentifier(self): """ If a subscription returns a subid, this should be available. """ xml = """ """ def subscribe(request): subscription = pubsub.Subscription(request.nodeIdentifier, request.subscriber, 'subscribed', subscriptionIdentifier='1234') return defer.succeed(subscription) def cb(element): self.assertEqual('1234', element.subscription.getAttribute('subid')) self.resource.subscribe = subscribe verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_unsubscribe(self): """ A successful unsubscription should return an empty response. """ xml = """ """ def unsubscribe(request): return defer.succeed(None) def cb(element): self.assertIdentical(None, element) self.resource.unsubscribe = unsubscribe verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_unsubscribeSubscriptionIdentifier(self): """ A successful unsubscription with subid should return an empty response. """ xml = """ """ def unsubscribe(request): self.assertEqual('1234', request.subscriptionIdentifier) return defer.succeed(None) def cb(element): self.assertIdentical(None, element) self.resource.unsubscribe = unsubscribe verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_optionsGet(self): """ Getting subscription options is not supported. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_optionsSet(self): """ Setting subscription options is not supported. """ xml = """ http://jabber.org/protocol/pubsub#subscribe_options 1 """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_subscriptions(self): """ A subscriptions request should result in L{PubSubService.subscriptions} being called and the result prepared for the response. """ xml = """ """ def subscriptions(request): subscription = pubsub.Subscription('test', JID('user@example.org'), 'subscribed') return defer.succeed([subscription]) def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB, element.uri) self.assertEqual(NS_PUBSUB, element.subscriptions.uri) children = list(element.subscriptions.elements()) self.assertEqual(1, len(children)) subscription = children[0] self.assertEqual('subscription', subscription.name) self.assertEqual(NS_PUBSUB, subscription.uri, NS_PUBSUB) self.assertEqual('user@example.org', subscription['jid']) self.assertEqual('test', subscription['node']) self.assertEqual('subscribed', subscription['subscription']) self.resource.subscriptions = subscriptions verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_subscriptionsWithSubscriptionIdentifier(self): """ A subscriptions request response should include subids, if set. """ xml = """ """ def subscriptions(request): subscription = pubsub.Subscription('test', JID('user@example.org'), 'subscribed', subscriptionIdentifier='1234') return defer.succeed([subscription]) def cb(element): subscription = element.subscriptions.subscription self.assertEqual('1234', subscription['subid']) self.resource.subscriptions = subscriptions verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_affiliations(self): """ A subscriptions request should result in L{PubSubService.affiliations} being called and the result prepared for the response. """ xml = """ """ def affiliations(request): affiliation = ('test', 'owner') return defer.succeed([affiliation]) def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB, element.uri) self.assertEqual(NS_PUBSUB, element.affiliations.uri) children = list(element.affiliations.elements()) self.assertEqual(1, len(children)) affiliation = children[0] self.assertEqual('affiliation', affiliation.name) self.assertEqual(NS_PUBSUB, affiliation.uri) self.assertEqual('test', affiliation['node']) self.assertEqual('owner', affiliation['affiliation']) self.resource.affiliations = affiliations verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_create(self): """ Replies to create node requests don't return the created node. """ xml = """ """ def create(request): return defer.succeed(request.nodeIdentifier) def cb(element): self.assertIdentical(None, element) self.resource.create = create verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_createChanged(self): """ Replies to create node requests return the created node if changed. """ xml = """ """ def create(request): return defer.succeed(u'myrenamednode') def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB, element.uri) self.assertEqual(NS_PUBSUB, element.create.uri) self.assertEqual(u'myrenamednode', element.create.getAttribute('node')) self.resource.create = create verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_createInstant(self): """ Replies to create instant node requests return the created node. """ xml = """ """ def create(request): return defer.succeed(u'random') def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB, element.uri) self.assertEqual(NS_PUBSUB, element.create.uri) self.assertEqual(u'random', element.create.getAttribute('node')) self.resource.create = create verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_createWithConfig(self): """ On a node create with configuration request the Data Form is parsed and L{PubSubResource.create} is called with the passed options. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def getConfigurationOptions(): return { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"} } def create(request): self.assertEqual({'pubsub#deliver_payloads': False, 'pubsub#persist_items': True}, request.options.getValues()) return defer.succeed(None) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.create = create verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_default(self): """ A default request returns default options filtered by available fields. """ xml = """ """ fieldDefs = { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"} } def getConfigurationOptions(): return fieldDefs def default(request): return defer.succeed({'pubsub#persist_items': 'false', 'x-myfield': '1'}) def cb(element): self.assertEquals('pubsub', element.name) self.assertEquals(NS_PUBSUB_OWNER, element.uri) self.assertEquals(NS_PUBSUB_OWNER, element.default.uri) form = data_form.Form.fromElement(element.default.x) self.assertEquals(NS_PUBSUB_NODE_CONFIG, form.formNamespace) form.typeCheck(fieldDefs) self.assertIn('pubsub#persist_items', form.fields) self.assertFalse(form.fields['pubsub#persist_items'].value) self.assertNotIn('x-myfield', form.fields) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.default = default verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_defaultUnknownNodeType(self): """ Unknown node types yield non-acceptable. Both C{getConfigurationOptions} and C{default} must not be called. """ xml = """ http://jabber.org/protocol/pubsub#node_config unknown """ def getConfigurationOptions(): self.fail("Unexpected call to getConfigurationOptions") def default(request): self.fail("Unexpected call to default") def cb(result): self.assertEquals('not-acceptable', result.condition) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.default = default verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_configureGet(self): """ On a node configuration get requestL{PubSubResource.configureGet} is called and results in a data form with the configuration. """ xml = """ """ def getConfigurationOptions(): return { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"}, "pubsub#owner": {"type": "jid-single", "label": "Owner of the node"} } def configureGet(request): return defer.succeed({'pubsub#deliver_payloads': '0', 'pubsub#persist_items': '1', 'pubsub#owner': JID('user@example.org'), 'x-myfield': 'a'}) def cb(element): self.assertEqual('pubsub', element.name) self.assertEqual(NS_PUBSUB_OWNER, element.uri) self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri) form = data_form.Form.fromElement(element.configure.x) self.assertEqual(NS_PUBSUB_NODE_CONFIG, form.formNamespace) fields = form.fields self.assertIn('pubsub#deliver_payloads', fields) field = fields['pubsub#deliver_payloads'] self.assertEqual('boolean', field.fieldType) field.typeCheck() self.assertEqual(False, field.value) self.assertIn('pubsub#persist_items', fields) field = fields['pubsub#persist_items'] self.assertEqual('boolean', field.fieldType) field.typeCheck() self.assertEqual(True, field.value) self.assertIn('pubsub#owner', fields) field = fields['pubsub#owner'] self.assertEqual('jid-single', field.fieldType) field.typeCheck() self.assertEqual(JID('user@example.org'), field.value) self.assertNotIn('x-myfield', fields) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.configureGet = configureGet verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_configureSet(self): """ On a node configuration set request the Data Form is parsed and L{PubSubResource.configureSet} is called with the passed options. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def getConfigurationOptions(): return { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"} } def configureSet(request): self.assertEqual({'pubsub#deliver_payloads': False, 'pubsub#persist_items': True}, request.options.getValues()) return defer.succeed(None) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.configureSet = configureSet verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_configureSetCancel(self): """ The node configuration is cancelled, L{PubSubResource.configureSet} not called. """ xml = """ http://jabber.org/protocol/pubsub#node_config """ def configureSet(request): self.fail("Unexpected call to setConfiguration") self.resource.configureSet = configureSet verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_configureSetIgnoreUnknown(self): """ On a node configuration set request unknown fields should be ignored. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def getConfigurationOptions(): return { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"} } def configureSet(request): self.assertEquals(['pubsub#deliver_payloads'], request.options.keys()) self.resource.getConfigurationOptions = getConfigurationOptions self.resource.configureSet = configureSet verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_configureSetBadFormType(self): """ On a node configuration set request unknown fields should be ignored. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def cb(result): self.assertEquals('bad-request', result.condition) self.assertEqual("Unexpected form type 'result'", result.text) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_items(self): """ On a items request, return all items for the given node. """ xml = """ """ def items(request): return defer.succeed([pubsub.Item('current')]) def cb(element): self.assertEqual(NS_PUBSUB, element.uri) self.assertEqual(NS_PUBSUB, element.items.uri) self.assertEqual(1, len(element.items.children)) item = element.items.children[-1] self.assertTrue(domish.IElement.providedBy(item)) self.assertEqual('item', item.name) self.assertEqual(NS_PUBSUB, item.uri) self.assertEqual('current', item['id']) self.resource.items = items verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_retract(self): """ A retract request should result in L{PubSubResource.retract} being called. """ xml = """ """ def retract(request): return defer.succeed(None) self.resource.retract = retract verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_purge(self): """ A purge request should result in L{PubSubResource.purge} being called. """ xml = """ """ def purge(request): return defer.succeed(None) self.resource.purge = purge verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_on_delete(self): """ A delete request should result in L{PubSubResource.delete} being called. """ xml = """ """ def delete(request): return defer.succeed(None) self.resource.delete = delete verify.verifyObject(iwokkel.IPubSubResource, self.resource) return self.handleRequest(xml) def test_notifyPublish(self): """ Publish notifications are sent to the subscribers. """ subscriber = JID('user@example.org') subscriptions = [pubsub.Subscription('test', subscriber, 'subscribed')] items = [pubsub.Item('current')] notifications = [(subscriber, subscriptions, items)] self.service.notifyPublish(JID('pubsub.example.org'), 'test', notifications) message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertIdentical(None, message.uri) self.assertEquals('user@example.org', message['to']) self.assertEquals('pubsub.example.org', message['from']) self.assertTrue(message.event) self.assertEquals(NS_PUBSUB_EVENT, message.event.uri) self.assertTrue(message.event.items) self.assertEquals(NS_PUBSUB_EVENT, message.event.items.uri) self.assertTrue(message.event.items.hasAttribute('node')) self.assertEquals('test', message.event.items['node']) itemElements = list(domish.generateElementsQNamed( message.event.items.children, 'item', NS_PUBSUB_EVENT)) self.assertEquals(1, len(itemElements)) self.assertEquals('current', itemElements[0].getAttribute('id')) def test_notifyPublishCollection(self): """ Publish notifications are sent to the subscribers of collections. The node the item was published to is on the C{items} element, while the subscribed-to node is in the C{'Collections'} SHIM header. """ subscriber = JID('user@example.org') subscriptions = [pubsub.Subscription('', subscriber, 'subscribed')] items = [pubsub.Item('current')] notifications = [(subscriber, subscriptions, items)] self.service.notifyPublish(JID('pubsub.example.org'), 'test', notifications) message = self.stub.output[-1] self.assertTrue(message.event.items.hasAttribute('node')) self.assertEquals('test', message.event.items['node']) headers = shim.extractHeaders(message) self.assertIn('Collection', headers) self.assertIn('', headers['Collection']) def test_notifyDelete(self): """ Subscribers should be sent a delete notification. """ subscriptions = [JID('user@example.org')] self.service.notifyDelete(JID('pubsub.example.org'), 'test', subscriptions) message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertIdentical(None, message.uri) self.assertEquals('user@example.org', message['to']) self.assertEquals('pubsub.example.org', message['from']) self.assertTrue(message.event) self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) self.assertTrue(message.event.delete) self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) self.assertTrue(message.event.delete.hasAttribute('node')) self.assertEqual('test', message.event.delete['node']) def test_notifyDeleteRedirect(self): """ Subscribers should be sent a delete notification with redirect. """ redirectURI = 'xmpp:pubsub.example.org?;node=test2' subscriptions = [JID('user@example.org')] self.service.notifyDelete(JID('pubsub.example.org'), 'test', subscriptions, redirectURI) message = self.stub.output[-1] self.assertEquals('message', message.name) self.assertIdentical(None, message.uri) self.assertEquals('user@example.org', message['to']) self.assertEquals('pubsub.example.org', message['from']) self.assertTrue(message.event) self.assertEqual(NS_PUBSUB_EVENT, message.event.uri) self.assertTrue(message.event.delete) self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri) self.assertTrue(message.event.delete.hasAttribute('node')) self.assertEqual('test', message.event.delete['node']) self.assertTrue(message.event.delete.redirect) self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri) self.assertTrue(message.event.delete.redirect.hasAttribute('uri')) self.assertEqual(redirectURI, message.event.delete.redirect['uri']) def test_on_subscriptionsGet(self): """ Getting subscription options is not supported. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('manage-subscriptions', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_subscriptionsSet(self): """ Setting subscription options is not supported. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('manage-subscriptions', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_affiliationsGet(self): """ Getting node affiliations should have. """ xml = """ """ def affiliationsGet(request): self.assertEquals('test', request.nodeIdentifier) return defer.succeed({JID('user@example.org'): 'owner'}) def cb(element): self.assertEquals(u'pubsub', element.name) self.assertEquals(NS_PUBSUB_OWNER, element.uri) self.assertEquals(NS_PUBSUB_OWNER, element.affiliations.uri) self.assertEquals(u'test', element.affiliations[u'node']) children = list(element.affiliations.elements()) self.assertEquals(1, len(children)) affiliation = children[0] self.assertEquals(u'affiliation', affiliation.name) self.assertEquals(NS_PUBSUB_OWNER, affiliation.uri) self.assertEquals(u'user@example.org', affiliation[u'jid']) self.assertEquals(u'owner', affiliation[u'affiliation']) self.resource.affiliationsGet = affiliationsGet verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_affiliationsGetEmptyNode(self): """ Getting node affiliations without node should assume empty node. """ xml = """ """ def affiliationsGet(request): self.assertIdentical('', request.nodeIdentifier) return defer.succeed({}) def cb(element): self.assertFalse(element.affiliations.hasAttribute(u'node')) self.resource.affiliationsGet = affiliationsGet verify.verifyObject(iwokkel.IPubSubResource, self.resource) d = self.handleRequest(xml) d.addCallback(cb) return d def test_on_affiliationsSet(self): """ Setting node affiliations has the affiliations to be modified. """ xml = """ """ def affiliationsSet(request): self.assertEquals(u'test', request.nodeIdentifier) otherJID = JID(u'other@example.org') self.assertIn(otherJID, request.affiliations) self.assertEquals(u'publisher', request.affiliations[otherJID]) self.resource.affiliationsSet = affiliationsSet return self.handleRequest(xml) def test_on_affiliationsSetBareJID(self): """ Affiliations are always on the bare JID. """ xml = """ """ def affiliationsSet(request): otherJID = JID(u'other@example.org') self.assertIn(otherJID, request.affiliations) self.resource.affiliationsSet = affiliationsSet return self.handleRequest(xml) def test_on_affiliationsSetMultipleForSameEntity(self): """ Setting node affiliations can only have one item per entity. """ xml = """ """ def cb(result): self.assertEquals('bad-request', result.condition) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_affiliationsSetMissingJID(self): """ Setting node affiliations must include a JID per affiliation. """ xml = """ """ def cb(result): self.assertEquals('bad-request', result.condition) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_on_affiliationsSetMissingAffiliation(self): """ Setting node affiliations must include an affiliation. """ xml = """ """ def cb(result): self.assertEquals('bad-request', result.condition) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d class PubSubServiceWithoutResourceTest(unittest.TestCase, TestableRequestHandlerMixin): def setUp(self): self.stub = XmlStreamStub() self.service = pubsub.PubSubService() self.service.send = self.stub.xmlstream.send def test_getDiscoInfo(self): """ Test getDiscoInfo calls getNodeInfo and returns some minimal info. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn(('pubsub', 'service'), discoInfo.identities) self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features) d = self.service.getDiscoInfo(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_publish(self): """ Non-overridden L{PubSubService.publish} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('publish', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_subscribe(self): """ Non-overridden L{PubSubService.subscribe} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('subscribe', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_unsubscribe(self): """ Non-overridden L{PubSubService.unsubscribe} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('subscribe', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_subscriptions(self): """ Non-overridden L{PubSubService.subscriptions} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-subscriptions', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_affiliations(self): """ Non-overridden L{PubSubService.affiliations} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-affiliations', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_create(self): """ Non-overridden L{PubSubService.create} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('create-nodes', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_getDefaultConfiguration(self): """ Non-overridden L{PubSubService.getDefaultConfiguration} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-default', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_getConfiguration(self): """ Non-overridden L{PubSubService.getConfiguration} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('config-node', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_setConfiguration(self): """ Non-overridden L{PubSubService.setConfiguration} yields unsupported error. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('config-node', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_setConfigurationOptionsDict(self): """ Options should be passed as a dictionary, not a form. """ xml = """ http://jabber.org/protocol/pubsub#node_config 0 1 """ def getConfigurationOptions(): return { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"} } def setConfiguration(requestor, service, nodeIdentifier, options): self.assertIn('pubsub#deliver_payloads', options) self.assertFalse(options['pubsub#deliver_payloads']) self.assertIn('pubsub#persist_items', options) self.assertTrue(options['pubsub#persist_items']) self.service.getConfigurationOptions = getConfigurationOptions self.service.setConfiguration = setConfiguration return self.handleRequest(xml) def test_items(self): """ Non-overridden L{PubSubService.items} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-items', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_retract(self): """ Non-overridden L{PubSubService.retract} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retract-items', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_purge(self): """ Non-overridden L{PubSubService.purge} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('purge-nodes', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_delete(self): """ Non-overridden L{PubSubService.delete} yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('delete-nodes', result.appCondition['feature']) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_unknown(self): """ Unknown verb yields unsupported error. """ xml = """ """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) d = self.handleRequest(xml) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d class PubSubResourceTest(unittest.TestCase): def setUp(self): self.resource = pubsub.PubSubResource() def test_interface(self): """ Do instances of L{pubsub.PubSubResource} provide L{iwokkel.IPubSubResource}? """ verify.verifyObject(iwokkel.IPubSubResource, self.resource) def test_getNodes(self): """ Default getNodes returns an empty list. """ def cb(nodes): self.assertEquals([], nodes) d = self.resource.getNodes(JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_publish(self): """ Non-overridden L{PubSubResource.publish} yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('publish', result.appCondition['feature']) d = self.resource.publish(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_subscribe(self): """ Non-overridden subscriptions yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('subscribe', result.appCondition['feature']) d = self.resource.subscribe(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_unsubscribe(self): """ Non-overridden unsubscribe yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('subscribe', result.appCondition['feature']) d = self.resource.unsubscribe(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_subscriptions(self): """ Non-overridden subscriptions yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-subscriptions', result.appCondition['feature']) d = self.resource.subscriptions(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_affiliations(self): """ Non-overridden affiliations yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-affiliations', result.appCondition['feature']) d = self.resource.affiliations(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_create(self): """ Non-overridden create yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('create-nodes', result.appCondition['feature']) d = self.resource.create(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_default(self): """ Non-overridden default yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-default', result.appCondition['feature']) d = self.resource.default(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_configureGet(self): """ Non-overridden configureGet yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('config-node', result.appCondition['feature']) d = self.resource.configureGet(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_configureSet(self): """ Non-overridden configureSet yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('config-node', result.appCondition['feature']) d = self.resource.configureSet(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_items(self): """ Non-overridden items yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retrieve-items', result.appCondition['feature']) d = self.resource.items(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_retract(self): """ Non-overridden retract yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('retract-items', result.appCondition['feature']) d = self.resource.retract(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_purge(self): """ Non-overridden purge yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('purge-nodes', result.appCondition['feature']) d = self.resource.purge(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_delete(self): """ Non-overridden delete yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('delete-nodes', result.appCondition['feature']) d = self.resource.delete(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_affiliationsGet(self): """ Non-overridden owner affiliations get yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('modify-affiliations', result.appCondition['feature']) d = self.resource.affiliationsGet(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d def test_affiliationsSet(self): """ Non-overridden owner affiliations set yields unsupported error. """ def cb(result): self.assertEquals('feature-not-implemented', result.condition) self.assertEquals('unsupported', result.appCondition.name) self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) self.assertEquals('modify-affiliations', result.appCondition['feature']) d = self.resource.affiliationsSet(pubsub.PubSubRequest()) self.assertFailure(d, error.StanzaError) d.addCallback(cb) return d wokkel-0.7.1/wokkel/test/test_data_form.py0000775000175000017500000013571211707215356021450 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for {wokkel.data_form}. """ from zope.interface import verify from zope.interface.common.mapping import IIterableMapping from twisted.trial import unittest from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from wokkel import data_form NS_X_DATA = 'jabber:x:data' class OptionTest(unittest.TestCase): """ Tests for L{data_form.Option}. """ def test_toElement(self): """ An option is an option element with a value child with the option value. """ option = data_form.Option('value') element = option.toElement() self.assertEqual('option', element.name) self.assertEqual(NS_X_DATA, element.uri) self.assertEqual(NS_X_DATA, element.value.uri) self.assertEqual('value', unicode(element.value)) self.assertFalse(element.hasAttribute('label')) def test_toElementLabel(self): """ A label is rendered as an attribute on the option element. """ option = data_form.Option('value', 'label') element = option.toElement() self.assertEqual('option', element.name) self.assertEqual(NS_X_DATA, element.uri) self.assertEqual(NS_X_DATA, element.value.uri) self.assertEqual('value', unicode(element.value)) self.assertEqual('label', element['label']) def test_fromElement(self): """ An option has a child element with the option value. """ element = domish.Element((NS_X_DATA, 'option')) element.addElement('value', content='value') option = data_form.Option.fromElement(element) self.assertEqual('value', option.value) self.assertIdentical(None, option.label) def test_fromElementLabel(self): """ An option label is an attribute on the option element. """ element = domish.Element((NS_X_DATA, 'option')) element.addElement('value', content='value') element['label'] = 'label' option = data_form.Option.fromElement(element) self.assertEqual('label', option.label) def test_fromElementNoValue(self): """ An option MUST have a value. """ element = domish.Element((NS_X_DATA, 'option')) self.assertRaises(data_form.Error, data_form.Option.fromElement, element) def test_repr(self): """ The representation of an Option is equal to how it is created. """ option = data_form.Option('value', 'label') self.assertEqual("""Option('value', 'label')""", repr(option)) class FieldTest(unittest.TestCase): """ Tests for L{data_form.Field}. """ def test_basic(self): """ Test basic field initialization. """ field = data_form.Field(var='test') self.assertEqual('text-single', field.fieldType) self.assertEqual('test', field.var) def test_labelAndOptions(self): """ The label should be set, even if there are options with labels as dict. """ field = data_form.Field(label='test', options={'test2': 'test 2', 'test3': 'test 3'}) self.assertEqual('test', field.label) def test_repr(self): """ The repr of a field should be equal to its initialization. """ field = data_form.Field('list-single', var='test', label='label', desc='desc', required=True, value='test', options=[data_form.Option('test')]) self.assertEqual("""Field(fieldType='list-single', """ """var='test', label='label', """ """desc='desc', required=True, """ """values=['test'], """ """options=[Option('test')])""", repr(field)) def test_toElement(self): """ Test rendering to a DOM. """ field = data_form.Field(var='test') element = field.toElement() self.assertTrue(domish.IElement.providedBy(element)) self.assertEquals('field', element.name) self.assertEquals(NS_X_DATA, element.uri) self.assertEquals('text-single', element.getAttribute('type', 'text-single')) self.assertEquals('test', element['var']) self.assertEquals([], element.children) def test_toElementTypeNotTextSingle(self): """ Always render the field type, if different from text-single. """ field = data_form.Field('hidden', var='test') element = field.toElement() self.assertEquals('hidden', element.getAttribute('type')) def test_toElementSingleValue(self): """ A single value should yield only one value element. """ field = data_form.Field('list-multi', var='test', value='test') element = field.toElement() children = list(element.elements()) self.assertEqual(1, len(children)) def test_toElementMultipleValues(self): """ A field with no type and multiple values should render all values. """ field = data_form.Field('list-multi', var='test', values=['test', 'test2']) element = field.toElement() children = list(element.elements()) self.assertEqual(2, len(children)) def test_toElementAsForm(self): """ Always render the field type, if asForm is True. """ field = data_form.Field(var='test') element = field.toElement(True) self.assertEquals('text-single', element.getAttribute('type')) def test_toElementOptions(self): """ Test rendering to a DOM with options. """ field = data_form.Field('list-single', var='test') field.options = [data_form.Option(u'option1'), data_form.Option(u'option2')] element = field.toElement(True) self.assertEqual(2, len(element.children)) def test_toElementLabel(self): """ Test rendering to a DOM with a label. """ field = data_form.Field(var='test', label=u'my label') element = field.toElement(True) self.assertEqual(u'my label', element.getAttribute('label')) def test_toElementDescription(self): """ Test rendering to a DOM with options. """ field = data_form.Field(var='test', desc=u'My desc') element = field.toElement(True) self.assertEqual(1, len(element.children)) child = element.children[0] self.assertEqual('desc', child.name) self.assertEqual(NS_X_DATA, child.uri) self.assertEqual(u'My desc', unicode(child)) def test_toElementRequired(self): """ Test rendering to a DOM with options. """ field = data_form.Field(var='test', required=True) element = field.toElement(True) self.assertEqual(1, len(element.children)) child = element.children[0] self.assertEqual('required', child.name) self.assertEqual(NS_X_DATA, child.uri) def test_toElementJID(self): """ A JID value should render to text. """ field = data_form.Field(fieldType='jid-single', var='test', value=jid.JID(u'test@example.org')) element = field.toElement() self.assertEqual(u'test@example.org', unicode(element.value)) def test_toElementJIDTextSingle(self): """ A JID value should render to text if field type is text-single. """ field = data_form.Field(fieldType='text-single', var='test', value=jid.JID(u'test@example.org')) element = field.toElement() self.assertEqual(u'test@example.org', unicode(element.value)) def test_toElementBoolean(self): """ A boolean value should render to text. """ field = data_form.Field(fieldType='boolean', var='test', value=True) element = field.toElement() self.assertEqual(u'true', unicode(element.value)) def test_toElementBooleanTextSingle(self): """ A boolean value should render to text if the field type is text-single. """ field = data_form.Field(var='test', value=True) element = field.toElement() self.assertEqual(u'true', unicode(element.value)) def test_toElementNoType(self): """ A field with no type should not have a type attribute. """ field = data_form.Field(None, var='test', value='test') element = field.toElement() self.assertFalse(element.hasAttribute('type')) def test_toElementNoTypeMultipleValues(self): """ A field with no type and multiple values should render all values. """ field = data_form.Field(None, var='test', values=['test', 'test2']) element = field.toElement() self.assertFalse(element.hasAttribute('type')) children = list(element.elements()) self.assertEqual(2, len(children)) def test_typeCheckNoFieldName(self): """ A field not of type fixed must have a var. """ field = data_form.Field(fieldType='list-single') self.assertRaises(data_form.FieldNameRequiredError, field.typeCheck) def test_typeCheckTooManyValues(self): """ Expect an exception if too many values are set, depending on type. """ field = data_form.Field(fieldType='list-single', var='test', values=[u'value1', u'value2']) self.assertRaises(data_form.TooManyValuesError, field.typeCheck) def test_typeCheckBooleanFalse(self): """ Test possible False values for a boolean field. """ field = data_form.Field(fieldType='boolean', var='test') for value in (False, 0, '0', 'false', 'False', []): field.value = value field.typeCheck() self.assertIsInstance(field.value, bool) self.assertFalse(field.value) def test_typeCheckBooleanTrue(self): """ Test possible True values for a boolean field. """ field = data_form.Field(fieldType='boolean', var='test') for value in (True, 1, '1', 'true', 'True', ['test']): field.value = value field.typeCheck() self.assertIsInstance(field.value, bool) self.assertTrue(field.value) def test_typeCheckBooleanBad(self): """ A bad value for a boolean field should raise a ValueError """ field = data_form.Field(fieldType='boolean', var='test') field.value = 'test' self.assertRaises(ValueError, field.typeCheck) def test_typeCheckJID(self): """ The value of jid field should be a JID or coercable to one. """ field = data_form.Field(fieldType='jid-single', var='test', value=jid.JID('test@example.org')) field.typeCheck() def test_typeCheckJIDString(self): """ The string value of jid field should be coercable into a JID. """ field = data_form.Field(fieldType='jid-single', var='test', value='test@example.org') field.typeCheck() self.assertEquals(jid.JID(u'test@example.org'), field.value) def test_typeCheckJIDBad(self): """ An invalid JID string should raise an exception. """ field = data_form.Field(fieldType='jid-single', var='test', value='test@@example.org') self.assertRaises(jid.InvalidFormat, field.typeCheck) def test_fromElementType(self): element = domish.Element((NS_X_DATA, 'field')) element['type'] = 'fixed' field = data_form.Field.fromElement(element) self.assertEquals('fixed', field.fieldType) def test_fromElementNoType(self): element = domish.Element((NS_X_DATA, 'field')) field = data_form.Field.fromElement(element) self.assertEquals(None, field.fieldType) def test_fromElementValueTextSingle(self): """ Parsed text-single field values should be of type C{unicode}. """ element = domish.Element((NS_X_DATA, 'field')) element['type'] = 'text-single' element.addElement('value', content=u'text') field = data_form.Field.fromElement(element) self.assertEquals('text', field.value) def test_fromElementValueJID(self): """ Parsed jid-single field values should be of type C{unicode}. """ element = domish.Element((NS_X_DATA, 'field')) element['type'] = 'jid-single' element.addElement('value', content=u'user@example.org') field = data_form.Field.fromElement(element) self.assertEquals(u'user@example.org', field.value) def test_fromElementValueJIDMalformed(self): """ Parsed jid-single field values should be of type C{unicode}. No validation should be done at this point, so invalid JIDs should also be passed as-is. """ element = domish.Element((NS_X_DATA, 'field')) element['type'] = 'jid-single' element.addElement('value', content=u'@@') field = data_form.Field.fromElement(element) self.assertEquals(u'@@', field.value) def test_fromElementValueBoolean(self): """ Parsed boolean field values should be of type C{unicode}. """ element = domish.Element((NS_X_DATA, 'field')) element['type'] = 'boolean' element.addElement('value', content=u'false') field = data_form.Field.fromElement(element) self.assertEquals(u'false', field.value) def test_fromElementDesc(self): """ Field descriptions are in a desc child element. """ element = domish.Element((NS_X_DATA, 'field')) element.addElement('desc', content=u'My description') field = data_form.Field.fromElement(element) self.assertEqual(u'My description', field.desc) def test_fromElementOption(self): """ Field descriptions are in a desc child element. """ element = domish.Element((NS_X_DATA, 'field')) element.addElement('option').addElement('value', content=u'option1') element.addElement('option').addElement('value', content=u'option2') field = data_form.Field.fromElement(element) self.assertEqual(2, len(field.options)) def test_fromElementRequired(self): """ Required fields have a required child element. """ element = domish.Element((NS_X_DATA, 'field')) element.addElement('required') field = data_form.Field.fromElement(element) self.assertTrue(field.required) def test_fromElementChildOtherNamespace(self): """ Child elements from another namespace are ignored. """ element = domish.Element((NS_X_DATA, 'field')) element['var'] = 'test' element.addElement(('myns', 'value')) field = data_form.Field.fromElement(element) self.assertIdentical(None, field.value) def test_fromDict(self): """ A named field with a value can be created by providing a dictionary. """ fieldDict = {'var': 'test', 'value': 'text'} field = data_form.Field.fromDict(fieldDict) self.assertEqual('test', field.var) self.assertEqual('text', field.value) def test_fromDictFieldType(self): """ The field type is set using the key 'type'. """ fieldDict = {'type': 'boolean'} field = data_form.Field.fromDict(fieldDict) self.assertEqual('boolean', field.fieldType) def test_fromDictOptions(self): """ The field options are set using the key 'options'. The options are represented as a dictionary keyed by option, with the optional label as value. """ fieldDict = {'options': {'value1': 'label1', 'value2': 'label2'}} field = data_form.Field.fromDict(fieldDict) self.assertEqual(2, len(field.options)) options = {} for option in field.options: options[option.value] = option.label self.assertEqual(options, fieldDict['options']) class FormTest(unittest.TestCase): """ Tests for L{data_form.Form}. """ def test_formType(self): """ A form has a type. """ form = data_form.Form('result') self.assertEqual('result', form.formType) def test_toElement(self): """ The toElement method returns a form's DOM representation. """ form = data_form.Form('result') element = form.toElement() self.assertTrue(domish.IElement.providedBy(element)) self.assertEquals('x', element.name) self.assertEquals(NS_X_DATA, element.uri) self.assertEquals('result', element['type']) self.assertEquals([], element.children) def test_toElementTitle(self): """ A title is rendered as a child element with the title as CDATA. """ form = data_form.Form('form', title='Bot configuration') element = form.toElement() elements = list(element.elements()) self.assertEqual(1, len(elements)) title = elements[0] self.assertEqual('title', title.name) self.assertEqual(NS_X_DATA, title.uri) self.assertEqual('Bot configuration', unicode(title)) def test_toElementInstructions(self): """ Instructions are rendered as child elements with CDATA. """ form = data_form.Form('form', instructions=['Fill out this form!']) element = form.toElement() elements = list(element.elements()) self.assertEqual(1, len(elements)) instructions = elements[0] self.assertEqual('instructions', instructions.name) self.assertEqual(NS_X_DATA, instructions.uri) self.assertEqual('Fill out this form!', unicode(instructions)) def test_toElementInstructionsMultiple(self): """ Instructions render as one element per instruction, in order. """ form = data_form.Form('form', instructions=['Fill out this form!', 'no really']) element = form.toElement() elements = list(element.elements()) self.assertEqual(2, len(elements)) instructions1 = elements[0] instructions2 = elements[1] self.assertEqual('instructions', instructions1.name) self.assertEqual(NS_X_DATA, instructions1.uri) self.assertEqual('Fill out this form!', unicode(instructions1)) self.assertEqual('instructions', instructions2.name) self.assertEqual(NS_X_DATA, instructions2.uri) self.assertEqual('no really', unicode(instructions2)) def test_toElementFormType(self): """ The form type is rendered as a hidden field with name FORM_TYPE. """ form = data_form.Form('form', formNamespace='jabber:bot') element = form.toElement() elements = list(element.elements()) self.assertEqual(1, len(elements)) formTypeField = elements[0] self.assertEqual('field', formTypeField.name) self.assertEqual(NS_X_DATA, formTypeField.uri) self.assertEqual('FORM_TYPE', formTypeField['var']) self.assertEqual('hidden', formTypeField['type']) self.assertEqual('jabber:bot', unicode(formTypeField.value)) def test_toElementFields(self): """ Fields are rendered as child elements, in order. """ fields = [data_form.Field('fixed', value='Section 1'), data_form.Field('text-single', var='botname', label='The name of your bot'), data_form.Field('text-multi', var='description', label='Helpful description of your bot'), data_form.Field('boolean', var='public', label='Public bot?', required=True) ] form = data_form.Form('form', fields=fields) element = form.toElement() elements = list(element.elements()) self.assertEqual(4, len(elements)) for field in elements: self.assertEqual('field', field.name) self.assertEqual(NS_X_DATA, field.uri) # Check order self.assertEqual('fixed', elements[0]['type']) self.assertEqual('botname', elements[1]['var']) self.assertEqual('description', elements[2]['var']) self.assertEqual('public', elements[3]['var']) def test_fromElement(self): """ C{fromElement} creates a L{data_form.Form} from a DOM representation. """ element = domish.Element((NS_X_DATA, 'x')) element['type'] = 'result' form = data_form.Form.fromElement(element) self.assertEquals('result', form.formType) self.assertEquals(None, form.title) self.assertEquals([], form.instructions) self.assertEquals({}, form.fields) def test_fromElementInvalidElementName(self): """ Bail if the passed element does not have the correct name. """ element = domish.Element((NS_X_DATA, 'form')) self.assertRaises(Exception, data_form.Form.fromElement, element) def test_fromElementInvalidElementURI(self): """ Bail if the passed element does not have the correct namespace. """ element = domish.Element(('myns', 'x')) self.assertRaises(Exception, data_form.Form.fromElement, element) def test_fromElementTitle(self): element = domish.Element((NS_X_DATA, 'x')) element.addElement('title', content='My title') form = data_form.Form.fromElement(element) self.assertEquals('My title', form.title) def test_fromElementInstructions(self): element = domish.Element((NS_X_DATA, 'x')) element.addElement('instructions', content='instruction') form = data_form.Form.fromElement(element) self.assertEquals(['instruction'], form.instructions) def test_fromElementInstructions2(self): element = domish.Element((NS_X_DATA, 'x')) element.addElement('instructions', content='instruction 1') element.addElement('instructions', content='instruction 2') form = data_form.Form.fromElement(element) self.assertEquals(['instruction 1', 'instruction 2'], form.instructions) def test_fromElementOneField(self): element = domish.Element((NS_X_DATA, 'x')) element.addElement('field') form = data_form.Form.fromElement(element) self.assertEquals(1, len(form.fieldList)) self.assertNotIn('field', form.fields) def test_fromElementTwoFields(self): element = domish.Element((NS_X_DATA, 'x')) element.addElement('field')['var'] = 'field1' element.addElement('field')['var'] = 'field2' form = data_form.Form.fromElement(element) self.assertEquals(2, len(form.fieldList)) self.assertIn('field1', form.fields) self.assertEquals('field1', form.fieldList[0].var) self.assertIn('field2', form.fields) self.assertEquals('field2', form.fieldList[1].var) def test_fromElementFormType(self): """ The form type is a hidden field named FORM_TYPE. """ element = domish.Element((NS_X_DATA, 'x')) field = element.addElement('field') field['var'] = 'FORM_TYPE' field['type'] = 'hidden' field.addElement('value', content='myns') form = data_form.Form.fromElement(element) self.assertNotIn('FORM_TYPE', form.fields) self.assertEqual('myns', form.formNamespace) def test_fromElementFormTypeNotHidden(self): """ A non-hidden field named FORM_TYPE does not set the form type. """ element = domish.Element((NS_X_DATA, 'x')) field = element.addElement('field') field['var'] = 'FORM_TYPE' field.addElement('value', content='myns') form = data_form.Form.fromElement(element) self.assertIn('FORM_TYPE', form.fields) self.assertIdentical(None, form.formNamespace) def test_fromElementChildOtherNamespace(self): """ Child elements from another namespace are ignored. """ element = domish.Element((NS_X_DATA, 'x')) element['type'] = 'result' field = element.addElement(('myns', 'field')) field['var'] = 'test' form = data_form.Form.fromElement(element) self.assertEqual(0, len(form.fields)) def test_repr(self): """ The repr of a form should be equal to its initialization. """ form = data_form.Form('form', title='title', instructions=['instr'], formNamespace='myns', fields=[data_form.Field('fixed', value='test')]) self.assertEqual("""Form(formType='form', title='title', """ """instructions=['instr'], formNamespace='myns', """ """fields=[Field(fieldType='fixed', """ """values=['test'])])""", repr(form)) def test_addField(self): """ A field should occur in fieldList. """ form = data_form.Form('result') field = data_form.Field('fixed', value='Section 1') form.addField(field) self.assertEqual([field], form.fieldList) def test_addFieldTwice(self): """ Fields occur in fieldList in the order they were added. """ form = data_form.Form('result') field1 = data_form.Field('fixed', value='Section 1') field2 = data_form.Field('fixed', value='Section 2') form.addField(field1) form.addField(field2) self.assertEqual([field1, field2], form.fieldList) def test_addFieldNotNamed(self): """ A non-named field should not occur in fields. """ form = data_form.Form('result') field = data_form.Field('fixed', value='Section 1') form.addField(field) self.assertEqual({}, form.fields) def test_addFieldNamed(self): """ A named field should occur in fields. """ form = data_form.Form('result') field = data_form.Field(var='test') form.addField(field) self.assertEqual({'test': field}, form.fields) def test_addFieldTwiceNamed(self): """ A second named field should occur in fields. """ form = data_form.Form('result') field1 = data_form.Field(var='test') field2 = data_form.Field(var='test2') form.addField(field2) form.addField(field1) self.assertEqual({'test': field1, 'test2': field2}, form.fields) def test_addFieldSameName(self): """ A named field cannot occur twice. """ form = data_form.Form('result') field1 = data_form.Field(var='test', value='value') field2 = data_form.Field(var='test', value='value2') form.addField(field1) self.assertRaises(data_form.Error, form.addField, field2) def test_removeField(self): """ A removed field should not occur in fieldList. """ form = data_form.Form('result') field = data_form.Field('fixed', value='Section 1') form.addField(field) form.removeField(field) self.assertNotIn(field, form.fieldList) def test_removeFieldNamed(self): """ A removed named field should not occur in fields. """ form = data_form.Form('result') field = data_form.Field(var='test', value='test1') form.addField(field) form.removeField(field) self.assertNotIn('test', form.fields) def test_makeField(self): """ Fields can be created from a dict of values and a dict of field defs. """ fieldDefs = { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, "pubsub#deliver_payloads": {"type": "boolean", "label": "Deliver payloads with event notifications"}, "pubsub#creator": {"type": "jid-single", "label": "The JID of the node creator"}, "pubsub#description": {"type": "text-single", "label": "A description of the node"}, "pubsub#owner": {"type": "jid-single", "label": "Owner of the node"}, } values = {'pubsub#deliver_payloads': '0', 'pubsub#persist_items': True, 'pubsub#description': 'a great node', 'pubsub#owner': jid.JID('user@example.org'), 'x-myfield': ['a', 'b']} form = data_form.Form('submit') form.makeFields(values, fieldDefs) # Check that the expected fields have been created self.assertIn('pubsub#deliver_payloads', form.fields) self.assertIn('pubsub#persist_items', form.fields) self.assertIn('pubsub#description', form.fields) self.assertIn('pubsub#owner', form.fields) # This field is not created because there is no value for it. self.assertNotIn('pubsub#creator', form.fields) # This field is not created because it does not appear in fieldDefs # and filterUnknown defaults to True self.assertNotIn('x-myfield', form.fields) # Check properties the created fields self.assertEqual('boolean', form.fields['pubsub#deliver_payloads'].fieldType) self.assertEqual('0', form.fields['pubsub#deliver_payloads'].value) self.assertEqual('Deliver payloads with event notifications', form.fields['pubsub#deliver_payloads'].label) self.assertEqual(True, form.fields['pubsub#persist_items'].value) def test_makeFieldNotFilterUnknown(self): """ Fields can be created from a dict of values and a dict of field defs. """ fieldDefs = { "pubsub#persist_items": {"type": "boolean", "label": "Persist items to storage"}, } values = {'x-myfield': ['a', 'b']} form = data_form.Form('submit') form.makeFields(values, fieldDefs, filterUnknown=False) field = form.fields['x-myfield'] self.assertEqual(None, field.fieldType) self.assertEqual(values, form.getValues()) def test_makeFieldsUnknownTypeJID(self): """ Without type, a single JID value sets field type jid-single. """ values = {'pubsub#creator': jid.JID('user@example.org')} form = data_form.Form('result') form.makeFields(values) field = form.fields['pubsub#creator'] self.assertEqual(None, field.fieldType) self.assertEqual(values, form.getValues()) def test_makeFieldsUnknownTypeJIDMulti(self): """ Without type, multiple JID values sets field type jid-multi. """ values = {'pubsub#contact': [jid.JID('user@example.org'), jid.JID('other@example.org')]} form = data_form.Form('result') form.makeFields(values) field = form.fields['pubsub#contact'] self.assertEqual(None, field.fieldType) self.assertEqual(values, form.getValues()) def test_makeFieldsUnknownTypeBoolean(self): """ Without type, a boolean value sets field type boolean. """ values = {'pubsub#persist_items': True} form = data_form.Form('result') form.makeFields(values) field = form.fields['pubsub#persist_items'] self.assertEqual(None, field.fieldType) self.assertEqual(values, form.getValues()) def test_makeFieldsUnknownTypeListMulti(self): """ Without type, multiple values sets field type list-multi. """ values = {'pubsub#show-values': ['chat', 'online', 'away']} form = data_form.Form('result') form.makeFields(values) field = form.fields['pubsub#show-values'] self.assertEqual(None, field.fieldType) self.assertEqual(values, form.getValues()) def test_interface(self): """ L{Form}s act as a read-only dictionary. """ form = data_form.Form('submit') verify.verifyObject(IIterableMapping, form) def test_getitem(self): """ Using Form as a mapping will yield the value of fields keyed by name. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True), data_form.Field('list-multi', var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) self.assertEqual('The Jabber Bot', form['botname']) self.assertTrue(form['public']) self.assertEqual(['news', 'search'], form['features']) def test_getitemOneValueTypeMulti(self): """ A single value for a multi-value field type is returned in a list. """ fields = [data_form.Field('list-multi', var='features', values=['news'])] form = data_form.Form('submit', fields=fields) self.assertEqual(['news'], form['features']) def test_getitemMultipleValuesNoType(self): """ Multiple values for a field without type are returned in a list. """ fields = [data_form.Field(None, var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) self.assertEqual(['news', 'search'], form['features']) def test_getitemMultipleValuesTypeSingle(self): """ Multiple values for a single-value field type returns the first value. """ fields = [data_form.Field('text-single', var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) self.assertEqual('news', form['features']) def test_get(self): """ Getting the value of a known field succeeds. """ fields = [data_form.Field(var='botname', value='The Jabber Bot')] form = data_form.Form('submit', fields=fields) self.assertEqual('The Jabber Bot', form.get('botname')) def test_getUnknownNone(self): """ Getting the value of a unknown field returns None. """ fields = [data_form.Field(var='botname', value='The Jabber Bot')] form = data_form.Form('submit', fields=fields) self.assertIdentical(None, form.get('features')) def test_getUnknownDefault(self): """ Getting the value of a unknown field returns specified default. """ fields = [data_form.Field(var='botname', value='The Jabber Bot')] form = data_form.Form('submit', fields=fields) self.assertTrue(form.get('public', True)) def test_contains(self): """ A form contains a known field. """ fields = [data_form.Field(var='botname', value='The Jabber Bot')] form = data_form.Form('submit', fields=fields) self.assertIn('botname', form) def test_containsNot(self): """ A form does not contains an unknown field. """ fields = [data_form.Field(var='botname', value='The Jabber Bot')] form = data_form.Form('submit', fields=fields) self.assertNotIn('features', form) def test_iterkeys(self): """ Iterating over the keys of a form yields all field names. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True), data_form.Field('list-multi', var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) self.assertEqual(set(['botname', 'public', 'features']), set(form.iterkeys())) def test_itervalues(self): """ Iterating over the values of a form yields all field values. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True)] form = data_form.Form('submit', fields=fields) self.assertEqual(set(['The Jabber Bot', True]), set(form.itervalues())) def test_iteritems(self): """ Iterating over the values of a form yields all item tuples. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True)] form = data_form.Form('submit', fields=fields) self.assertEqual(set([('botname', 'The Jabber Bot'), ('public', True)]), set(form.iteritems())) def test_keys(self): """ Getting the keys of a form yields a list of field names. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True), data_form.Field('list-multi', var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) keys = form.keys() self.assertIsInstance(keys, list) self.assertEqual(set(['botname', 'public', 'features']), set(keys)) def test_values(self): """ Getting the values of a form yields a list of field values. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True)] form = data_form.Form('submit', fields=fields) values = form.values() self.assertIsInstance(values, list) self.assertEqual(set(['The Jabber Bot', True]), set(values)) def test_items(self): """ Iterating over the values of a form yields a list of all item tuples. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True)] form = data_form.Form('submit', fields=fields) items = form.items() self.assertIsInstance(items, list) self.assertEqual(set([('botname', 'The Jabber Bot'), ('public', True)]), set(items)) def test_getValues(self): """ L{Form.getValues} returns a dict of all field values. """ fields = [data_form.Field(var='botname', value='The Jabber Bot'), data_form.Field('boolean', var='public', value=True), data_form.Field('list-multi', var='features', values=['news', 'search'])] form = data_form.Form('submit', fields=fields) self.assertEqual({'botname': 'The Jabber Bot', 'public': True, 'features': ['news', 'search']}, form.getValues()) def test_typeCheckKnownFieldChecked(self): """ Known fields are type checked. """ checked = [] fieldDefs = {"pubsub#description": {"type": "text-single", "label": "A description of the node"}} form = data_form.Form('submit') form.addField(data_form.Field(var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs) self.assertEqual([None], checked) def test_typeCheckKnownFieldNoType(self): """ Known fields without a type get the type of the field definition. """ checked = [] fieldDefs = {"pubsub#description": {"type": "text-single", "label": "A description of the node"}} form = data_form.Form('submit') form.addField(data_form.Field(None, var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs) self.assertEqual('text-single', field.fieldType) self.assertEqual([None], checked) def test_typeCheckWrongFieldType(self): """ A field should have the same type as the field definition. """ checked = [] fieldDefs = {"pubsub#description": {"type": "text-single", "label": "A description of the node"}} form = data_form.Form('submit') form.addField(data_form.Field('list-single', var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) self.assertRaises(TypeError, form.typeCheck, fieldDefs) self.assertEqual([], checked) def test_typeCheckDefaultTextSingle(self): """ If a field definition has no type, use text-single. """ checked = [] fieldDefs = {"pubsub#description": {"label": "A description of the node"}} form = data_form.Form('submit') form.addField(data_form.Field('text-single', var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs) self.assertEqual([None], checked) def test_typeCheckUnknown(self): """ Unknown fields are checked, not removed if filterUnknown False. """ checked = [] fieldDefs = {} form = data_form.Form('submit') form.addField(data_form.Field('list-single', var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs, filterUnknown=False) self.assertIn('pubsub#description', form.fields) self.assertEqual([None], checked) def test_typeCheckUnknownNoType(self): """ Unknown fields without type are not checked. """ checked = [] fieldDefs = {} form = data_form.Form('submit') form.addField(data_form.Field(None, var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs, filterUnknown=False) self.assertIn('pubsub#description', form.fields) self.assertEqual([], checked) def test_typeCheckUnknownRemoved(self): """ Unknown fields are not checked, and removed if filterUnknown True. """ checked = [] fieldDefs = {} form = data_form.Form('submit') form.addField(data_form.Field('list-single', var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck(fieldDefs, filterUnknown=True) self.assertNotIn('pubsub#description', form.fields) self.assertEqual([], checked) class FindFormTest(unittest.TestCase): """ Tests for L{data_form.findForm}. """ def test_findForm(self): element = domish.Element((None, 'test')) theForm = data_form.Form('submit', formNamespace='myns') element.addChild(theForm.toElement()) form = data_form.findForm(element, 'myns') self.assertEqual('myns', form.formNamespace) def test_noFormType(self): element = domish.Element((None, 'test')) otherForm = data_form.Form('submit') element.addChild(otherForm.toElement()) form = data_form.findForm(element, 'myns') self.assertIdentical(None, form) def test_noFormTypeCancel(self): """ Cancelled forms don't have a FORM_TYPE field, the first is returned. """ element = domish.Element((None, 'test')) cancelledForm = data_form.Form('cancel') element.addChild(cancelledForm.toElement()) form = data_form.findForm(element, 'myns') self.assertEqual('cancel', form.formType) def test_otherFormType(self): """ Forms with other FORM_TYPEs are ignored. """ element = domish.Element((None, 'test')) otherForm = data_form.Form('submit', formNamespace='otherns') element.addChild(otherForm.toElement()) form = data_form.findForm(element, 'myns') self.assertIdentical(None, form) def test_otherFormTypeCancel(self): """ Cancelled forms with another FORM_TYPE are ignored. """ element = domish.Element((None, 'test')) cancelledForm = data_form.Form('cancel', formNamespace='otherns') element.addChild(cancelledForm.toElement()) form = data_form.findForm(element, 'myns') self.assertIdentical(None, form) def test_noElement(self): """ When None is passed as element, None is returned. """ element = None form = data_form.findForm(element, 'myns') self.assertIdentical(None, form) def test_noForm(self): """ When no child element is a form, None is returned. """ element = domish.Element((None, 'test')) form = data_form.findForm(element, 'myns') self.assertIdentical(None, form) def test_typeCheckNoFieldDefs(self): """ If there are no field defs, an empty dictionary is assumed. """ checked = [] form = data_form.Form('submit') form.addField(data_form.Field('list-single', var='pubsub#description', value='a node')) field = form.fields['pubsub#description'] field.typeCheck = lambda : checked.append(None) form.typeCheck() self.assertIn('pubsub#description', form.fields) self.assertEqual([None], checked) wokkel-0.7.1/wokkel/test/__init__.py0000775000175000017500000000012711707215356020203 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel}. """ wokkel-0.7.1/wokkel/test/test_subprotocols.py0000775000175000017500000007421412074262111022236 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.subprotocols} """ from zope.interface.verify import verifyObject from twisted.trial import unittest from twisted.test import proto_helpers from twisted.internet import defer, task from twisted.internet.error import ConnectionDone from twisted.python import failure from twisted.words.xish import domish from twisted.words.protocols.jabber import error, ijabber, xmlstream from wokkel import generic, subprotocols class DeprecationTest(unittest.TestCase): """ Deprecation test for L{wokkel.subprotocols}. """ def lookForDeprecationWarning(self, testmethod, attributeName, newName): """ Importing C{testmethod} emits a deprecation warning. """ warningsShown = self.flushWarnings([testmethod]) self.assertEqual(len(warningsShown), 1) self.assertIdentical(warningsShown[0]['category'], DeprecationWarning) self.assertEqual( warningsShown[0]['message'], "wokkel.subprotocols." + attributeName + " " "was deprecated in Wokkel 0.7.0: Use " + newName + " instead.") def test_xmppHandlerCollection(self): """ L{subprotocols.XMPPHandlerCollection} is deprecated. """ from wokkel.subprotocols import XMPPHandlerCollection XMPPHandlerCollection self.lookForDeprecationWarning( self.test_xmppHandlerCollection, "XMPPHandlerCollection", "twisted.words.protocols.jabber.xmlstream." "XMPPHandlerCollection") class DummyFactory(object): """ Dummy XmlStream factory that only registers bootstrap observers. """ def __init__(self): self.callbacks = {} def addBootstrap(self, event, callback): self.callbacks[event] = callback class DummyXMPPHandler(subprotocols.XMPPHandler): """ Dummy XMPP subprotocol handler to count the methods are called on it. """ def __init__(self): self.doneMade = 0 self.doneInitialized = 0 self.doneLost = 0 def makeConnection(self, xs): self.connectionMade() def connectionMade(self): self.doneMade += 1 def connectionInitialized(self): self.doneInitialized += 1 def connectionLost(self, reason): self.doneLost += 1 class FailureReasonXMPPHandler(subprotocols.XMPPHandler): """ Dummy handler specifically for failure Reason tests. """ def __init__(self): self.gotFailureReason = False def connectionLost(self, reason): if isinstance(reason, failure.Failure): self.gotFailureReason = True class IQGetStanza(generic.Stanza): timeout = None stanzaKind = 'iq' stanzaType = 'get' stanzaID = 'test' class XMPPHandlerTest(unittest.TestCase): """ Tests for L{subprotocols.XMPPHandler}. """ def test_interface(self): """ L{xmlstream.XMPPHandler} implements L{ijabber.IXMPPHandler}. """ verifyObject(ijabber.IXMPPHandler, subprotocols.XMPPHandler()) def test_send(self): """ Test that data is passed on for sending by the stream manager. """ class DummyStreamManager(object): def __init__(self): self.outlist = [] def send(self, data): self.outlist.append(data) handler = subprotocols.XMPPHandler() handler.parent = DummyStreamManager() handler.send('') self.assertEquals([''], handler.parent.outlist) def test_makeConnection(self): """ Test that makeConnection saves the XML stream and calls connectionMade. """ class TestXMPPHandler(subprotocols.XMPPHandler): def connectionMade(self): self.doneMade = True handler = TestXMPPHandler() xs = xmlstream.XmlStream(xmlstream.Authenticator()) handler.makeConnection(xs) self.assertTrue(handler.doneMade) self.assertIdentical(xs, handler.xmlstream) def test_connectionLost(self): """ Test that connectionLost forgets the XML stream. """ handler = subprotocols.XMPPHandler() xs = xmlstream.XmlStream(xmlstream.Authenticator()) handler.makeConnection(xs) handler.connectionLost(Exception()) self.assertIdentical(None, handler.xmlstream) def test_request(self): """ A request is passed up to the stream manager. """ class DummyStreamManager(object): def __init__(self): self.requests = [] def request(self, request): self.requests.append(request) return defer.succeed(None) handler = subprotocols.XMPPHandler() handler.parent = DummyStreamManager() request = IQGetStanza() d = handler.request(request) self.assertEquals(1, len(handler.parent.requests)) self.assertIdentical(request, handler.parent.requests[-1]) return d class StreamManagerTest(unittest.TestCase): """ Tests for L{subprotocols.StreamManager}. """ def setUp(self): factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator()) self.clock = task.Clock() self.streamManager = subprotocols.StreamManager(factory, self.clock) self.xmlstream = factory.buildProtocol(None) self.transport = proto_helpers.StringTransport() self.xmlstream.transport = self.transport self.request = IQGetStanza() def _streamStarted(self): """ Bring the test stream to the initialized state. """ self.xmlstream.connectionMade() self.xmlstream.dataReceived( "") self.xmlstream.dispatch(self.xmlstream, "//event/stream/authd") def test_basic(self): """ Test correct initialization and setup of factory observers. """ factory = DummyFactory() sm = subprotocols.StreamManager(factory) self.assertIdentical(None, sm.xmlstream) self.assertEquals([], sm.handlers) self.assertEquals(sm._connected, sm.factory.callbacks['//event/stream/connected']) self.assertEquals(sm._authd, sm.factory.callbacks['//event/stream/authd']) self.assertEquals(sm._disconnected, sm.factory.callbacks['//event/stream/end']) self.assertEquals(sm.initializationFailed, sm.factory.callbacks['//event/xmpp/initfailed']) def test_connected(self): """ Test that protocol handlers have their connectionMade method called when the XML stream is connected. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._connected(xs) self.assertEquals(1, handler.doneMade) self.assertEquals(0, handler.doneInitialized) self.assertEquals(0, handler.doneLost) def test_connectedLogTrafficFalse(self): """ Test raw data functions unset when logTraffic is set to False. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._connected(xs) self.assertIdentical(None, xs.rawDataInFn) self.assertIdentical(None, xs.rawDataOutFn) def test_connectedLogTrafficTrue(self): """ Test raw data functions set when logTraffic is set to True. """ sm = self.streamManager sm.logTraffic = True handler = DummyXMPPHandler() handler.setHandlerParent(sm) xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._connected(xs) self.assertNotIdentical(None, xs.rawDataInFn) self.assertNotIdentical(None, xs.rawDataOutFn) def test_authd(self): """ Test that protocol handlers have their connectionInitialized method called when the XML stream is initialized. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._authd(xs) self.assertEquals(0, handler.doneMade) self.assertEquals(1, handler.doneInitialized) self.assertEquals(0, handler.doneLost) def test_disconnected(self): """ Protocol handlers have connectionLost called on stream disconnect. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) sm._disconnected(None) self.assertEquals(0, handler.doneMade) self.assertEquals(0, handler.doneInitialized) self.assertEquals(1, handler.doneLost) def test_disconnectedReason(self): """ A L{STREAM_END_EVENT} results in L{StreamManager} firing the handlers L{connectionLost} methods, passing a L{failure.Failure} reason. """ sm = self.streamManager handler = FailureReasonXMPPHandler() handler.setHandlerParent(sm) xmlstream.XmlStream(xmlstream.Authenticator()) sm._disconnected(failure.Failure(Exception("no reason"))) self.assertEquals(True, handler.gotFailureReason) def test_addHandler(self): """ Test the addition of a protocol handler while not connected. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) self.assertEquals(0, handler.doneMade) self.assertEquals(0, handler.doneInitialized) self.assertEquals(0, handler.doneLost) def test_addHandlerConnected(self): """ Adding a handler when connected doesn't call connectionInitialized. """ sm = self.streamManager xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._connected(xs) handler = DummyXMPPHandler() handler.setHandlerParent(sm) self.assertEquals(1, handler.doneMade) self.assertEquals(0, handler.doneInitialized) self.assertEquals(0, handler.doneLost) def test_addHandlerConnectedNested(self): """ Adding a handler in connectionMade doesn't cause 2nd call. """ class NestingHandler(DummyXMPPHandler): nestedHandler = None def connectionMade(self): DummyXMPPHandler.connectionMade(self) self.nestedHandler = DummyXMPPHandler() self.nestedHandler.setHandlerParent(self.parent) sm = self.streamManager xs = xmlstream.XmlStream(xmlstream.Authenticator()) handler = NestingHandler() handler.setHandlerParent(sm) sm._connected(xs) self.assertEquals(1, handler.doneMade) self.assertEquals(0, handler.doneInitialized) self.assertEquals(0, handler.doneLost) self.assertEquals(1, handler.nestedHandler.doneMade) self.assertEquals(0, handler.nestedHandler.doneInitialized) self.assertEquals(0, handler.nestedHandler.doneLost) def test_addHandlerInitialized(self): """ Test the addition of a protocol handler after the stream have been initialized. Make sure that the handler will have the connected stream passed via C{makeConnection} and have C{connectionInitialized} called. """ sm = self.streamManager xs = xmlstream.XmlStream(xmlstream.Authenticator()) sm._connected(xs) sm._authd(xs) handler = DummyXMPPHandler() handler.setHandlerParent(sm) self.assertEquals(1, handler.doneMade) self.assertEquals(1, handler.doneInitialized) self.assertEquals(0, handler.doneLost) def test_addHandlerInitializedNested(self): """ Adding a handler in connectionInitialized doesn't cause 2nd call. """ class NestingHandler(DummyXMPPHandler): nestedHandler = None def connectionInitialized(self): DummyXMPPHandler.connectionInitialized(self) self.nestedHandler = DummyXMPPHandler() self.nestedHandler.setHandlerParent(self.parent) sm = self.streamManager xs = xmlstream.XmlStream(xmlstream.Authenticator()) handler = NestingHandler() handler.setHandlerParent(sm) sm._connected(xs) sm._authd(xs) self.assertEquals(1, handler.doneMade) self.assertEquals(1, handler.doneInitialized) self.assertEquals(0, handler.doneLost) self.assertEquals(1, handler.nestedHandler.doneMade) self.assertEquals(1, handler.nestedHandler.doneInitialized) self.assertEquals(0, handler.nestedHandler.doneLost) def test_addHandlerConnectionLostNested(self): """ Adding a handler in connectionLost doesn't call connectionLost there. """ class NestingHandler(DummyXMPPHandler): nestedHandler = None def connectionLost(self, reason): DummyXMPPHandler.connectionLost(self, reason) self.nestedHandler = DummyXMPPHandler() self.nestedHandler.setHandlerParent(self.parent) sm = self.streamManager xs = xmlstream.XmlStream(xmlstream.Authenticator()) handler = NestingHandler() handler.setHandlerParent(sm) sm._connected(xs) sm._authd(xs) sm._disconnected(xs) self.assertEquals(1, handler.doneMade) self.assertEquals(1, handler.doneInitialized) self.assertEquals(1, handler.doneLost) self.assertEquals(0, handler.nestedHandler.doneMade) self.assertEquals(0, handler.nestedHandler.doneInitialized) self.assertEquals(0, handler.nestedHandler.doneLost) def test_removeHandler(self): """ Test removal of protocol handler. """ sm = self.streamManager handler = DummyXMPPHandler() handler.setHandlerParent(sm) handler.disownHandlerParent(sm) self.assertNotIn(handler, sm) self.assertIdentical(None, handler.parent) def test_sendInitialized(self): """ Test send when the stream has been initialized. The data should be sent directly over the XML stream. """ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator()) sm = subprotocols.StreamManager(factory) xs = factory.buildProtocol(None) xs.transport = proto_helpers.StringTransport() xs.connectionMade() xs.dataReceived("") xs.dispatch(xs, "//event/stream/authd") sm.send("") self.assertEquals("", xs.transport.value()) def test_sendNotConnected(self): """ Test send when there is no established XML stream. The data should be cached until an XML stream has been established and initialized. """ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator()) sm = subprotocols.StreamManager(factory) handler = DummyXMPPHandler() sm.addHandler(handler) xs = factory.buildProtocol(None) xs.transport = proto_helpers.StringTransport() sm.send("") self.assertEquals("", xs.transport.value()) self.assertEquals("", sm._packetQueue[0]) xs.connectionMade() self.assertEquals("", xs.transport.value()) self.assertEquals("", sm._packetQueue[0]) xs.dataReceived("") xs.dispatch(xs, "//event/stream/authd") self.assertEquals("", xs.transport.value()) self.assertFalse(sm._packetQueue) def test_sendNotInitialized(self): """ Test send when the stream is connected but not yet initialized. The data should be cached until the XML stream has been initialized. """ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator()) sm = subprotocols.StreamManager(factory) xs = factory.buildProtocol(None) xs.transport = proto_helpers.StringTransport() xs.connectionMade() xs.dataReceived("") sm.send("") self.assertEquals("", xs.transport.value()) self.assertEquals("", sm._packetQueue[0]) def test_sendDisconnected(self): """ Test send after XML stream disconnection. The data should be cached until a new XML stream has been established and initialized. """ factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator()) sm = subprotocols.StreamManager(factory) handler = DummyXMPPHandler() sm.addHandler(handler) xs = factory.buildProtocol(None) xs.connectionMade() xs.transport = proto_helpers.StringTransport() xs.connectionLost(None) sm.send("") self.assertEquals("", xs.transport.value()) self.assertEquals("", sm._packetQueue[0]) def test_requestSendInitialized(self): """ A request is sent out over the wire when the stream is initialized. """ self._streamStarted() self.streamManager.request(self.request) expected = u"" % self.request.stanzaID self.assertEquals(expected, self.transport.value()) def test_requestSendInitializedFreshID(self): """ A request without an ID gets a fresh one upon send. """ self._streamStarted() self.request.stanzaID = None self.streamManager.request(self.request) self.assertNotIdentical(None, self.request.stanzaID) expected = u"" % self.request.stanzaID self.assertEquals(expected, self.transport.value()) def test_requestSendNotConnected(self): """ A request is queued until a stream is initialized. """ handler = DummyXMPPHandler() self.streamManager.addHandler(handler) self.streamManager.request(self.request) expected = u"" xs = self.xmlstream self.assertEquals("", xs.transport.value()) xs.connectionMade() self.assertEquals("", xs.transport.value()) xs.dataReceived("") xs.dispatch(xs, "//event/stream/authd") self.assertEquals(expected, xs.transport.value()) self.assertFalse(self.streamManager._packetQueue) def test_requestResultResponse(self): """ A result response gets the request deferred fired with the response. """ def cb(result): self.assertEquals(result['type'], 'result') self._streamStarted() d = self.streamManager.request(self.request) d.addCallback(cb) xs = self.xmlstream xs.dataReceived("") return d def test_requestErrorResponse(self): """ An error response gets the request deferred fired with a failure. """ self._streamStarted() d = self.streamManager.request(self.request) self.assertFailure(d, error.StanzaError) xs = self.xmlstream xs.dataReceived("") return d def test_requestNonTrackedResponse(self): """ Test that untracked iq responses don't trigger any action. Untracked means that the id of the incoming response iq is not in the stream's C{iqDeferreds} dictionary. """ # Set up a fallback handler that checks the stanza's handled attribute. # If that is set to True, the iq tracker claims to have handled the # response. dispatched = [] def cb(iq): dispatched.append(iq) self._streamStarted() self.xmlstream.addObserver("/iq", cb, -1) # Receive an untracked iq response self.xmlstream.dataReceived("") self.assertEquals(1, len(dispatched)) self.assertFalse(getattr(dispatched[-1], 'handled', False)) def test_requestCleanup(self): """ Test if the deferred associated with an iq request is removed from the list kept in the L{XmlStream} object after it has been fired. """ self._streamStarted() d = self.streamManager.request(self.request) xs = self.xmlstream xs.dataReceived("") self.assertNotIn('test', self.streamManager._iqDeferreds) return d def test_requestDisconnectCleanup(self): """ Test if deferreds for iq's that haven't yet received a response have their errback called on stream disconnect. """ d = self.streamManager.request(self.request) xs = self.xmlstream xs.connectionLost(failure.Failure(ConnectionDone())) self.assertFailure(d, ConnectionDone) return d def test_requestNoModifyingDict(self): """ Test to make sure the errbacks cannot cause the iteration of the iqDeferreds to blow up in our face. """ def eb(failure): d = xmlstream.IQ(self.xmlstream).send() d.addErrback(eb) d = self.streamManager.request(self.request) d.addErrback(eb) self.xmlstream.connectionLost(failure.Failure(ConnectionDone())) return d def test_requestTimingOut(self): """ Test that an iq request with a defined timeout times out. """ self.request.timeout = 60 d = self.streamManager.request(self.request) self.assertFailure(d, xmlstream.TimeoutError) self.clock.pump([1, 60]) self.assertFalse(self.clock.calls) self.assertFalse(self.streamManager._iqDeferreds) return d def test_requestNotTimingOut(self): """ Test that an iq request with a defined timeout does not time out when a response was received before the timeout period elapsed. """ self._streamStarted() self.request.timeout = 60 d = self.streamManager.request(self.request) self.clock.callLater(1, self.xmlstream.dataReceived, "") self.clock.pump([1, 1]) self.assertFalse(self.clock.calls) return d def test_requestDisconnectTimeoutCancellation(self): """ Test if timeouts for iq's that haven't yet received a response are cancelled on stream disconnect. """ self.request.timeout = 60 d = self.streamManager.request(self.request) self.xmlstream.connectionLost(failure.Failure(ConnectionDone())) self.assertFailure(d, ConnectionDone) self.assertFalse(self.clock.calls) return d def test_requestNotIQ(self): """ The request stanza must be an iq. """ stanza = generic.Stanza() stanza.stanzaKind = 'message' d = self.streamManager.request(stanza) self.assertFailure(d, ValueError) def test_requestNotResult(self): """ The request stanza cannot be of type 'result'. """ stanza = generic.Stanza() stanza.stanzaKind = 'iq' stanza.stanzaType = 'result' d = self.streamManager.request(stanza) self.assertFailure(d, ValueError) def test_requestNotError(self): """ The request stanza cannot be of type 'error'. """ stanza = generic.Stanza() stanza.stanzaKind = 'iq' stanza.stanzaType = 'error' d = self.streamManager.request(stanza) self.assertFailure(d, ValueError) class DummyIQHandler(subprotocols.IQHandlerMixin): iqHandlers = {'/iq[@type="get"]': 'onGet'} def __init__(self): self.output = [] self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator()) self.xmlstream.send = self.output.append def send(self, obj): self.xmlstream.send(obj) class IQHandlerTest(unittest.TestCase): def test_match(self): """ Test that the matching handler gets called. """ class Handler(DummyIQHandler): called = False def onGet(self, iq): self.called = True iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) self.assertTrue(handler.called) def test_noMatch(self): """ Test that the matching handler gets called. """ class Handler(DummyIQHandler): called = False def onGet(self, iq): self.called = True iq = domish.Element((None, 'iq')) iq['type'] = 'set' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) self.assertFalse(handler.called) def test_success(self): """ Test response when the request is handled successfully. """ class Handler(DummyIQHandler): def onGet(self, iq): return None iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('result', response['type']) def test_successPayload(self): """ Test response when the request is handled successfully with payload. """ class Handler(DummyIQHandler): payload = domish.Element(('testns', 'foo')) def onGet(self, iq): return self.payload iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('result', response['type']) payload = response.elements().next() self.assertEqual(handler.payload, payload) def test_successDeferred(self): """ Test response when where the handler was a deferred. """ class Handler(DummyIQHandler): def onGet(self, iq): return defer.succeed(None) iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('result', response['type']) def test_failure(self): """ Test response when the request is handled unsuccessfully. """ class Handler(DummyIQHandler): def onGet(self, iq): raise error.StanzaError('forbidden') iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('error', response['type']) e = error.exceptionFromStanza(response) self.assertEquals('forbidden', e.condition) def test_failureUnknown(self): """ Test response when the request handler raises a non-stanza-error. """ class TestError(Exception): pass class Handler(DummyIQHandler): def onGet(self, iq): raise TestError() iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('error', response['type']) e = error.exceptionFromStanza(response) self.assertEquals('internal-server-error', e.condition) self.assertEquals(1, len(self.flushLoggedErrors(TestError))) def test_notImplemented(self): """ Test response when the request is recognised but not implemented. """ class Handler(DummyIQHandler): def onGet(self, iq): raise NotImplementedError() iq = domish.Element((None, 'iq')) iq['type'] = 'get' iq['id'] = 'r1' handler = Handler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('error', response['type']) e = error.exceptionFromStanza(response) self.assertEquals('feature-not-implemented', e.condition) def test_noHandler(self): """ Test when the request is not recognised. """ iq = domish.Element((None, 'iq')) iq['type'] = 'set' iq['id'] = 'r1' handler = DummyIQHandler() handler.handleRequest(iq) response = handler.output[-1] self.assertEquals(None, response.uri) self.assertEquals('iq', response.name) self.assertEquals('error', response['type']) e = error.exceptionFromStanza(response) self.assertEquals('feature-not-implemented', e.condition) wokkel-0.7.1/wokkel/test/test_ping.py0000775000175000017500000001334111707215356020442 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.ping}. """ from zope.interface import verify from twisted.internet import defer from twisted.trial import unittest from twisted.words.protocols.jabber.error import StanzaError from twisted.words.protocols.jabber.jid import JID from twisted.words.protocols.jabber.xmlstream import toResponse from wokkel import disco, iwokkel, ping from wokkel.generic import parseXml from wokkel.test.helpers import XmlStreamStub class PingClientProtocolTest(unittest.TestCase): """ Tests for L{ping.PingClientProtocol}. """ def setUp(self): self.stub = XmlStreamStub() self.protocol = ping.PingClientProtocol() self.protocol.xmlstream = self.stub.xmlstream self.protocol.connectionInitialized() def test_ping(self): """ Pinging a service should fire a deferred with None """ def cb(result): self.assertIdentical(None, result) d = self.protocol.ping(JID("example.com")) d.addCallback(cb) iq = self.stub.output[-1] self.assertEqual(u'example.com', iq.getAttribute(u'to')) self.assertEqual(u'get', iq.getAttribute(u'type')) self.assertEqual('urn:xmpp:ping', iq.ping.uri) response = toResponse(iq, u'result') self.stub.send(response) return d def test_pingWithSender(self): """ Pinging a service with a sender address should include that address. """ d = self.protocol.ping(JID("example.com"), sender=JID('user@example.com')) iq = self.stub.output[-1] self.assertEqual(u'user@example.com', iq.getAttribute(u'from')) response = toResponse(iq, u'result') self.stub.send(response) return d def test_pingNotSupported(self): """ Pinging a service should fire a deferred with None if not supported. """ def cb(result): self.assertIdentical(None, result) d = self.protocol.ping(JID("example.com")) d.addCallback(cb) iq = self.stub.output[-1] exc = StanzaError('service-unavailable') response = exc.toResponse(iq) self.stub.send(response) return d def test_pingStanzaError(self): """ Pinging a service should errback a deferred on other (stanza) errors. """ def cb(exc): self.assertEquals('item-not-found', exc.condition) d = self.protocol.ping(JID("example.com")) self.assertFailure(d, StanzaError) d.addCallback(cb) iq = self.stub.output[-1] exc = StanzaError('item-not-found') response = exc.toResponse(iq) self.stub.send(response) return d class PingHandlerTest(unittest.TestCase): """ Tests for L{ping.PingHandler}. """ def setUp(self): self.stub = XmlStreamStub() self.protocol = ping.PingHandler() self.protocol.xmlstream = self.stub.xmlstream self.protocol.connectionInitialized() def test_onPing(self): """ A ping should have a simple result response. """ xml = """ """ self.stub.send(parseXml(xml)) response = self.stub.output[-1] self.assertEquals('example.com', response.getAttribute('from')) self.assertEquals('test@example.com', response.getAttribute('to')) self.assertEquals('result', response.getAttribute('type')) def test_onPingHandled(self): """ The ping handler should mark the stanza as handled. """ xml = """ """ iq = parseXml(xml) self.stub.send(iq) self.assertTrue(iq.handled) def test_interfaceIDisco(self): """ The ping handler should provice Service Discovery information. """ verify.verifyObject(iwokkel.IDisco, self.protocol) def test_getDiscoInfo(self): """ The ping namespace should be returned as a supported feature. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertIn('urn:xmpp:ping', discoInfo.features) d = defer.maybeDeferred(self.protocol.getDiscoInfo, JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d def test_getDiscoInfoNode(self): """ The ping namespace should not be returned for a node. """ def cb(info): discoInfo = disco.DiscoInfo() for item in info: discoInfo.append(item) self.assertNotIn('urn:xmpp:ping', discoInfo.features) d = defer.maybeDeferred(self.protocol.getDiscoInfo, JID('user@example.org/home'), JID('pubsub.example.org'), 'test') d.addCallback(cb) return d def test_getDiscoItems(self): """ Items are not supported by this handler, so an empty list is expected. """ def cb(items): self.assertEquals(0, len(items)) d = defer.maybeDeferred(self.protocol.getDiscoItems, JID('user@example.org/home'), JID('pubsub.example.org'), '') d.addCallback(cb) return d wokkel-0.7.1/wokkel/test/test_generic.py0000775000175000017500000002266212074265316021126 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details. """ Tests for L{wokkel.generic}. """ from twisted.trial import unittest from twisted.words.xish import domish from twisted.words.protocols.jabber.jid import JID from wokkel import generic from wokkel.test.helpers import XmlStreamStub NS_VERSION = 'jabber:iq:version' class VersionHandlerTest(unittest.TestCase): """ Tests for L{wokkel.generic.VersionHandler}. """ def test_onVersion(self): """ Test response to incoming version request. """ self.stub = XmlStreamStub() self.protocol = generic.VersionHandler('Test', '0.1.0') self.protocol.xmlstream = self.stub.xmlstream self.protocol.send = self.stub.xmlstream.send self.protocol.connectionInitialized() iq = domish.Element((None, 'iq')) iq['from'] = 'user@example.org/Home' iq['to'] = 'example.org' iq['type'] = 'get' iq.addElement((NS_VERSION, 'query')) self.stub.send(iq) response = self.stub.output[-1] self.assertEquals('user@example.org/Home', response['to']) self.assertEquals('example.org', response['from']) self.assertEquals('result', response['type']) self.assertEquals(NS_VERSION, response.query.uri) elements = list(domish.generateElementsQNamed(response.query.children, 'name', NS_VERSION)) self.assertEquals(1, len(elements)) self.assertEquals('Test', unicode(elements[0])) elements = list(domish.generateElementsQNamed(response.query.children, 'version', NS_VERSION)) self.assertEquals(1, len(elements)) self.assertEquals('0.1.0', unicode(elements[0])) class XmlPipeTest(unittest.TestCase): """ Tests for L{wokkel.generic.XmlPipe}. """ def setUp(self): self.pipe = generic.XmlPipe() def test_sendFromSource(self): """ Send an element from the source and observe it from the sink. """ def cb(obj): called.append(obj) called = [] self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb) element = domish.Element(('testns', 'test')) self.pipe.source.send(element) self.assertEquals([element], called) def test_sendFromSink(self): """ Send an element from the sink and observe it from the source. """ def cb(obj): called.append(obj) called = [] self.pipe.source.addObserver('/test[@xmlns="testns"]', cb) element = domish.Element(('testns', 'test')) self.pipe.sink.send(element) self.assertEquals([element], called) class StanzaTest(unittest.TestCase): """ Tests for L{generic.Stanza}. """ def test_fromElement(self): xml = """ """ stanza = generic.Stanza.fromElement(generic.parseXml(xml)) self.assertEqual('chat', stanza.stanzaType) self.assertEqual(JID('other@example.org'), stanza.sender) self.assertEqual(JID('user@example.org'), stanza.recipient) def test_fromElementChildParser(self): """ Child elements for which no parser is defined are ignored. """ xml = """ """ class Message(generic.Stanza): childParsers = {('http://example.org/', 'x'): '_childParser_x'} elements = [] def _childParser_x(self, element): self.elements.append(element) message = Message.fromElement(generic.parseXml(xml)) self.assertEqual(1, len(message.elements)) def test_fromElementChildParserAll(self): """ Child elements for which no parser is defined are ignored. """ xml = """ """ class Message(generic.Stanza): childParsers = {None: '_childParser'} elements = [] def _childParser(self, element): self.elements.append(element) message = Message.fromElement(generic.parseXml(xml)) self.assertEqual(1, len(message.elements)) def test_fromElementChildParserUnknown(self): """ Child elements for which no parser is defined are ignored. """ xml = """ """ generic.Stanza.fromElement(generic.parseXml(xml)) class RequestTest(unittest.TestCase): """ Tests for L{generic.Request}. """ def setUp(self): self.request = generic.Request() def test_requestParser(self): """ The request's child element is passed to requestParser. """ xml = """ """ class VersionRequest(generic.Request): elements = [] def parseRequest(self, element): self.elements.append((element.uri, element.name)) request = VersionRequest.fromElement(generic.parseXml(xml)) self.assertEqual([(NS_VERSION, 'query')], request.elements) def test_toElementStanzaKind(self): """ A request is an iq stanza. """ element = self.request.toElement() self.assertIdentical(None, element.uri) self.assertEquals('iq', element.name) def test_toElementStanzaType(self): """ The request has type 'get'. """ self.assertEquals('get', self.request.stanzaType) element = self.request.toElement() self.assertEquals('get', element.getAttribute('type')) def test_toElementStanzaTypeSet(self): """ The request has type 'set'. """ self.request.stanzaType = 'set' element = self.request.toElement() self.assertEquals('set', element.getAttribute('type')) def test_toElementStanzaID(self): """ A request, when rendered, has an identifier. """ element = self.request.toElement() self.assertNotIdentical(None, self.request.stanzaID) self.assertEquals(self.request.stanzaID, element.getAttribute('id')) def test_toElementRecipient(self): """ A request without recipient, has no 'to' attribute. """ self.request = generic.Request(recipient=JID('other@example.org')) self.assertEquals(JID('other@example.org'), self.request.recipient) element = self.request.toElement() self.assertEquals(u'other@example.org', element.getAttribute('to')) def test_toElementRecipientNone(self): """ A request without recipient, has no 'to' attribute. """ element = self.request.toElement() self.assertFalse(element.hasAttribute('to')) def test_toElementSender(self): """ A request with sender, has a 'from' attribute. """ self.request = generic.Request(sender=JID('user@example.org')) self.assertEquals(JID('user@example.org'), self.request.sender) element = self.request.toElement() self.assertEquals(u'user@example.org', element.getAttribute('from')) def test_toElementSenderNone(self): """ A request without sender, has no 'from' attribute. """ element = self.request.toElement() self.assertFalse(element.hasAttribute('from')) def test_timeoutDefault(self): """ The default is no timeout. """ self.assertIdentical(None, self.request.timeout) class PrepareIDNNameTests(unittest.TestCase): """ Tests for L{wokkel.generic.prepareIDNName}. """ def test_bytestring(self): """ An ASCII-encoded byte string is left as-is. """ name = b"example.com" result = generic.prepareIDNName(name) self.assertEqual(b"example.com", result) def test_unicode(self): """ A unicode all-ASCII name is converted to an ASCII byte string. """ name = u"example.com" result = generic.prepareIDNName(name) self.assertEqual(b"example.com", result) def test_unicodeNonASCII(self): """ A unicode with non-ASCII is converted to its ACE equivalent. """ name = u"\u00e9chec.example.com" result = generic.prepareIDNName(name) self.assertEqual(b"xn--chec-9oa.example.com", result) def test_unicodeHalfwidthIdeographicFullStop(self): """ Exotic dots in unicode names are converted to Full Stop. """ name = u"\u00e9chec.example\uff61com" result = generic.prepareIDNName(name) self.assertEqual(b"xn--chec-9oa.example.com", result) def test_unicodeTrailingDot(self): """ Unicode names with trailing dots retain the trailing dot. L{encodings.idna.ToASCII} doesn't allow the empty string as the input, hence the implementation needs to strip a trailing dot, and re-add it after encoding the labels. """ name = u"example.com." result = generic.prepareIDNName(name) self.assertEqual(b"example.com.", result) wokkel-0.7.1/wokkel/__init__.py0000664000175000017500000000021112040670215017201 0ustar ralphmralphm00000000000000# Copyright (c) Ralph Meijer. # See LICENSE for details """ Wokkel. Support library for Twisted applications using XMPP protocols. """ wokkel-0.7.1/wokkel/pubsub.py0000775000175000017500000014332512014253401016756 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_pubsub -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP publish-subscribe protocol. This protocol is specified in U{XEP-0060}. """ from zope.interface import implements from twisted.internet import defer from twisted.python import log from twisted.words.protocols.jabber import jid, error from twisted.words.xish import domish from wokkel import disco, data_form, generic, shim from wokkel.compat import IQ from wokkel.subprotocols import IQHandlerMixin, XMPPHandler from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource # Iq get and set XPath queries IQ_GET = '/iq[@type="get"]' IQ_SET = '/iq[@type="set"]' # Publish-subscribe namespaces NS_PUBSUB = 'http://jabber.org/protocol/pubsub' NS_PUBSUB_EVENT = NS_PUBSUB + '#event' NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors' NS_PUBSUB_OWNER = NS_PUBSUB + "#owner" NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config" NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data" NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options" # XPath to match pubsub requests PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \ 'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \ '@xmlns="' + NS_PUBSUB_OWNER + '"]' class SubscriptionPending(Exception): """ Raised when the requested subscription is pending acceptance. """ class SubscriptionUnconfigured(Exception): """ Raised when the requested subscription needs to be configured before becoming active. """ class PubSubError(error.StanzaError): """ Exception with publish-subscribe specific condition. """ def __init__(self, condition, pubsubCondition, feature=None, text=None): appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) if feature: appCondition['feature'] = feature error.StanzaError.__init__(self, condition, text=text, appCondition=appCondition) class BadRequest(error.StanzaError): """ Bad request stanza error. """ def __init__(self, pubsubCondition=None, text=None): if pubsubCondition: appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) else: appCondition = None error.StanzaError.__init__(self, 'bad-request', text=text, appCondition=appCondition) class Unsupported(PubSubError): def __init__(self, feature, text=None): self.feature = feature PubSubError.__init__(self, 'feature-not-implemented', 'unsupported', feature, text) def __str__(self): message = PubSubError.__str__(self) message += ', feature %r' % self.feature return message class Subscription(object): """ A subscription to a node. @ivar nodeIdentifier: The identifier of the node subscribed to. The root node is denoted by C{None}. @type nodeIdentifier: C{unicode} @ivar subscriber: The subscribing entity. @type subscriber: L{jid.JID} @ivar state: The subscription state. One of C{'subscribed'}, C{'pending'}, C{'unconfigured'}. @type state: C{unicode} @ivar options: Optional list of subscription options. @type options: C{dict} @ivar subscriptionIdentifier: Optional subscription identifier. @type subscriptionIdentifier: C{unicode} """ def __init__(self, nodeIdentifier, subscriber, state, options=None, subscriptionIdentifier=None): self.nodeIdentifier = nodeIdentifier self.subscriber = subscriber self.state = state self.options = options or {} self.subscriptionIdentifier = subscriptionIdentifier @staticmethod def fromElement(element): return Subscription( element.getAttribute('node'), jid.JID(element.getAttribute('jid')), element.getAttribute('subscription'), subscriptionIdentifier=element.getAttribute('subid')) def toElement(self, defaultUri=None): """ Return the DOM representation of this subscription. @rtype: L{domish.Element} """ element = domish.Element((defaultUri, 'subscription')) if self.nodeIdentifier: element['node'] = self.nodeIdentifier element['jid'] = unicode(self.subscriber) element['subscription'] = self.state if self.subscriptionIdentifier: element['subid'] = self.subscriptionIdentifier return element class Item(domish.Element): """ Publish subscribe item. This behaves like an object providing L{domish.IElement}. Item payload can be added using C{addChild} or C{addRawXml}, or using the C{payload} keyword argument to C{__init__}. """ def __init__(self, id=None, payload=None): """ @param id: optional item identifier @type id: C{unicode} @param payload: optional item payload. Either as a domish element, or as serialized XML. @type payload: object providing L{domish.IElement} or C{unicode}. """ domish.Element.__init__(self, (None, 'item')) if id is not None: self['id'] = id if payload is not None: if isinstance(payload, basestring): self.addRawXml(payload) else: self.addChild(payload) class PubSubRequest(generic.Stanza): """ A publish-subscribe request. The set of instance variables used depends on the type of request. If a variable is not applicable or not passed in the request, its value is C{None}. @ivar verb: The type of publish-subscribe request. See C{_requestVerbMap}. @type verb: C{str}. @ivar affiliations: Affiliations to be modified. @type affiliations: C{set} @ivar items: The items to be published, as L{domish.Element}s. @type items: C{list} @ivar itemIdentifiers: Identifiers of the items to be retrieved or retracted. @type itemIdentifiers: C{set} @ivar maxItems: Maximum number of items to retrieve. @type maxItems: C{int}. @ivar nodeIdentifier: Identifier of the node the request is about. @type nodeIdentifier: C{unicode} @ivar nodeType: The type of node that should be created, or for which the configuration is retrieved. C{'leaf'} or C{'collection'}. @type nodeType: C{str} @ivar options: Configurations options for nodes, subscriptions and publish requests. @type options: L{data_form.Form} @ivar subscriber: The subscribing entity. @type subscriber: L{JID} @ivar subscriptionIdentifier: Identifier for a specific subscription. @type subscriptionIdentifier: C{unicode} @ivar subscriptions: Subscriptions to be modified, as a set of L{Subscription}. @type subscriptions: C{set} @ivar affiliations: Affiliations to be modified, as a dictionary of entity (L{JID} to affiliation (C{unicode}). @type affiliations: C{dict} """ verb = None affiliations = None items = None itemIdentifiers = None maxItems = None nodeIdentifier = None nodeType = None options = None subscriber = None subscriptionIdentifier = None subscriptions = None affiliations = None # Map request iq type and subelement name to request verb _requestVerbMap = { ('set', NS_PUBSUB, 'publish'): 'publish', ('set', NS_PUBSUB, 'subscribe'): 'subscribe', ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe', ('get', NS_PUBSUB, 'options'): 'optionsGet', ('set', NS_PUBSUB, 'options'): 'optionsSet', ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions', ('get', NS_PUBSUB, 'affiliations'): 'affiliations', ('set', NS_PUBSUB, 'create'): 'create', ('get', NS_PUBSUB_OWNER, 'default'): 'default', ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet', ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet', ('get', NS_PUBSUB, 'items'): 'items', ('set', NS_PUBSUB, 'retract'): 'retract', ('set', NS_PUBSUB_OWNER, 'purge'): 'purge', ('set', NS_PUBSUB_OWNER, 'delete'): 'delete', ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet', ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet', ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet', ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet', } # Map request verb to request iq type and subelement name _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems())) # Map request verb to parameter handler names _parameters = { 'publish': ['node', 'items'], 'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'], 'unsubscribe': ['nodeOrEmpty', 'jid', 'subidOrNone'], 'optionsGet': ['nodeOrEmpty', 'jid', 'subidOrNone'], 'optionsSet': ['nodeOrEmpty', 'jid', 'options', 'subidOrNone'], 'subscriptions': [], 'affiliations': [], 'create': ['nodeOrNone', 'configureOrNone'], 'default': ['default'], 'configureGet': ['nodeOrEmpty'], 'configureSet': ['nodeOrEmpty', 'configure'], 'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'], 'retract': ['node', 'itemIdentifiers'], 'purge': ['node'], 'delete': ['node'], 'affiliationsGet': ['nodeOrEmpty'], 'affiliationsSet': ['nodeOrEmpty', 'affiliations'], 'subscriptionsGet': ['nodeOrEmpty'], 'subscriptionsSet': [], } def __init__(self, verb=None): self.verb = verb def _parse_node(self, verbElement): """ Parse the required node identifier out of the verbElement. """ try: self.nodeIdentifier = verbElement["node"] except KeyError: raise BadRequest('nodeid-required') def _render_node(self, verbElement): """ Render the required node identifier on the verbElement. """ if not self.nodeIdentifier: raise Exception("Node identifier is required") verbElement['node'] = self.nodeIdentifier def _parse_nodeOrEmpty(self, verbElement): """ Parse the node identifier out of the verbElement. May be empty. """ self.nodeIdentifier = verbElement.getAttribute("node", '') def _render_nodeOrEmpty(self, verbElement): """ Render the node identifier on the verbElement. May be empty. """ if self.nodeIdentifier: verbElement['node'] = self.nodeIdentifier def _parse_nodeOrNone(self, verbElement): """ Parse the optional node identifier out of the verbElement. """ self.nodeIdentifier = verbElement.getAttribute("node") def _render_nodeOrNone(self, verbElement): """ Render the optional node identifier on the verbElement. """ if self.nodeIdentifier: verbElement['node'] = self.nodeIdentifier def _parse_items(self, verbElement): """ Parse items out of the verbElement for publish requests. """ self.items = [] for element in verbElement.elements(): if element.uri == NS_PUBSUB and element.name == 'item': self.items.append(element) def _render_items(self, verbElement): """ Render items into the verbElement for publish requests. """ if self.items: for item in self.items: item.uri = NS_PUBSUB verbElement.addChild(item) def _parse_jid(self, verbElement): """ Parse subscriber out of the verbElement for un-/subscribe requests. """ try: self.subscriber = jid.internJID(verbElement["jid"]) except KeyError: raise BadRequest('jid-required') def _render_jid(self, verbElement): """ Render subscriber into the verbElement for un-/subscribe requests. """ verbElement['jid'] = self.subscriber.full() def _parse_default(self, verbElement): """ Parse node type out of a request for the default node configuration. """ form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) if form is not None and form.formType == 'submit': values = form.getValues() self.nodeType = values.get('pubsub#node_type', 'leaf') else: self.nodeType = 'leaf' def _parse_configure(self, verbElement): """ Parse options out of a request for setting the node configuration. """ form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG) if form is not None: if form.formType in ('submit', 'cancel'): self.options = form else: raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) else: raise BadRequest(text="Missing configuration form") def _parse_configureOrNone(self, verbElement): """ Parse optional node configuration form in create request. """ for element in verbElement.parent.elements(): if element.uri == NS_PUBSUB and element.name == 'configure': form = data_form.findForm(element, NS_PUBSUB_NODE_CONFIG) if form is not None: if form.formType != 'submit': raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) else: form = data_form.Form('submit', formNamespace=NS_PUBSUB_NODE_CONFIG) self.options = form def _render_configureOrNone(self, verbElement): """ Render optional node configuration form in create request. """ if self.options is not None: configure = verbElement.parent.addElement('configure') configure.addChild(self.options.toElement()) def _parse_itemIdentifiers(self, verbElement): """ Parse item identifiers out of items and retract requests. """ self.itemIdentifiers = [] for element in verbElement.elements(): if element.uri == NS_PUBSUB and element.name == 'item': try: self.itemIdentifiers.append(element["id"]) except KeyError: raise BadRequest() def _render_itemIdentifiers(self, verbElement): """ Render item identifiers into items and retract requests. """ if self.itemIdentifiers: for itemIdentifier in self.itemIdentifiers: item = verbElement.addElement('item') item['id'] = itemIdentifier def _parse_maxItems(self, verbElement): """ Parse maximum items out of an items request. """ value = verbElement.getAttribute('max_items') if value: try: self.maxItems = int(value) except ValueError: raise BadRequest(text="Field max_items requires a positive " + "integer value") def _render_maxItems(self, verbElement): """ Render maximum items into an items request. """ if self.maxItems: verbElement['max_items'] = unicode(self.maxItems) def _parse_subidOrNone(self, verbElement): """ Parse subscription identifier out of a request. """ self.subscriptionIdentifier = verbElement.getAttribute("subid") def _render_subidOrNone(self, verbElement): """ Render subscription identifier into a request. """ if self.subscriptionIdentifier: verbElement['subid'] = self.subscriptionIdentifier def _parse_options(self, verbElement): """ Parse options form out of a subscription options request. """ form = data_form.findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS) if form is not None: if form.formType in ('submit', 'cancel'): self.options = form else: raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) else: raise BadRequest(text="Missing options form") def _render_options(self, verbElement): verbElement.addChild(self.options.toElement()) def _parse_optionsWithSubscribe(self, verbElement): for element in verbElement.parent.elements(): if element.name == 'options' and element.uri == NS_PUBSUB: form = data_form.findForm(element, NS_PUBSUB_SUBSCRIBE_OPTIONS) if form is not None: if form.formType != 'submit': raise BadRequest(text=u"Unexpected form type '%s'" % form.formType) else: form = data_form.Form('submit', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) self.options = form def _render_optionsWithSubscribe(self, verbElement): if self.options is not None: optionsElement = verbElement.parent.addElement('options') self._render_options(optionsElement) def _parse_affiliations(self, verbElement): self.affiliations = {} for element in verbElement.elements(): if (element.uri == NS_PUBSUB_OWNER and element.name == 'affiliation'): try: entity = jid.internJID(element['jid']).userhostJID() except KeyError: raise BadRequest(text='Missing jid attribute') if entity in self.affiliations: raise BadRequest(text='Multiple affiliations for an entity') try: affiliation = element['affiliation'] except KeyError: raise BadRequest(text='Missing affiliation attribute') self.affiliations[entity] = affiliation def parseElement(self, element): """ Parse the publish-subscribe verb and parameters out of a request. """ generic.Stanza.parseElement(self, element) verbs = [] verbElements = [] for child in element.pubsub.elements(): key = (self.stanzaType, child.uri, child.name) try: verb = self._requestVerbMap[key] except KeyError: continue verbs.append(verb) verbElements.append(child) if not verbs: raise NotImplementedError() if len(verbs) > 1: if 'optionsSet' in verbs and 'subscribe' in verbs: self.verb = 'subscribe' verbElement = verbElements[verbs.index('subscribe')] else: raise NotImplementedError() else: self.verb = verbs[0] verbElement = verbElements[0] for parameter in self._parameters[self.verb]: getattr(self, '_parse_%s' % parameter)(verbElement) def send(self, xs): """ Send this request to its recipient. This renders all of the relevant parameters for this specific requests into an L{IQ}, and invoke its C{send} method. This returns a deferred that fires upon reception of a response. See L{IQ} for details. @param xs: The XML stream to send the request on. @type xs: L{twisted.words.protocols.jabber.xmlstream.XmlStream} @rtype: L{defer.Deferred}. """ try: (self.stanzaType, childURI, childName) = self._verbRequestMap[self.verb] except KeyError: raise NotImplementedError() iq = IQ(xs, self.stanzaType) iq.addElement((childURI, 'pubsub')) verbElement = iq.pubsub.addElement(childName) if self.sender: iq['from'] = self.sender.full() if self.recipient: iq['to'] = self.recipient.full() for parameter in self._parameters[self.verb]: getattr(self, '_render_%s' % parameter)(verbElement) return iq.send() class PubSubEvent(object): """ A publish subscribe event. @param sender: The entity from which the notification was received. @type sender: L{jid.JID} @param recipient: The entity to which the notification was sent. @type recipient: L{wokkel.pubsub.ItemsEvent} @param nodeIdentifier: Identifier of the node the event pertains to. @type nodeIdentifier: C{unicode} @param headers: SHIM headers, see L{wokkel.shim.extractHeaders}. @type headers: C{dict} """ def __init__(self, sender, recipient, nodeIdentifier, headers): self.sender = sender self.recipient = recipient self.nodeIdentifier = nodeIdentifier self.headers = headers class ItemsEvent(PubSubEvent): """ A publish-subscribe event that signifies new, updated and retracted items. @param items: List of received items as domish elements. @type items: C{list} of L{domish.Element} """ def __init__(self, sender, recipient, nodeIdentifier, items, headers): PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers) self.items = items class DeleteEvent(PubSubEvent): """ A publish-subscribe event that signifies the deletion of a node. """ redirectURI = None class PurgeEvent(PubSubEvent): """ A publish-subscribe event that signifies the purging of a node. """ class PubSubClient(XMPPHandler): """ Publish subscribe client protocol. """ implements(IPubSubClient) def connectionInitialized(self): self.xmlstream.addObserver('/message/event[@xmlns="%s"]' % NS_PUBSUB_EVENT, self._onEvent) def _onEvent(self, message): if message.getAttribute('type') == 'error': return try: sender = jid.JID(message["from"]) recipient = jid.JID(message["to"]) except KeyError: return actionElement = None for element in message.event.elements(): if element.uri == NS_PUBSUB_EVENT: actionElement = element if not actionElement: return eventHandler = getattr(self, "_onEvent_%s" % actionElement.name, None) if eventHandler: headers = shim.extractHeaders(message) eventHandler(sender, recipient, actionElement, headers) message.handled = True def _onEvent_items(self, sender, recipient, action, headers): nodeIdentifier = action["node"] items = [element for element in action.elements() if element.name in ('item', 'retract')] event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers) self.itemsReceived(event) def _onEvent_delete(self, sender, recipient, action, headers): nodeIdentifier = action["node"] event = DeleteEvent(sender, recipient, nodeIdentifier, headers) if action.redirect: event.redirectURI = action.redirect.getAttribute('uri') self.deleteReceived(event) def _onEvent_purge(self, sender, recipient, action, headers): nodeIdentifier = action["node"] event = PurgeEvent(sender, recipient, nodeIdentifier, headers) self.purgeReceived(event) def itemsReceived(self, event): pass def deleteReceived(self, event): pass def purgeReceived(self, event): pass def createNode(self, service, nodeIdentifier=None, options=None, sender=None): """ Create a publish subscribe node. @param service: The publish subscribe service to create the node at. @type service: L{JID} @param nodeIdentifier: Optional suggestion for the id of the node. @type nodeIdentifier: C{unicode} @param options: Optional node configuration options. @type options: C{dict} """ request = PubSubRequest('create') request.recipient = service request.nodeIdentifier = nodeIdentifier request.sender = sender if options: form = data_form.Form(formType='submit', formNamespace=NS_PUBSUB_NODE_CONFIG) form.makeFields(options) request.options = form def cb(iq): try: new_node = iq.pubsub.create["node"] except AttributeError: # the suggested node identifier was accepted new_node = nodeIdentifier return new_node d = request.send(self.xmlstream) d.addCallback(cb) return d def deleteNode(self, service, nodeIdentifier, sender=None): """ Delete a publish subscribe node. @param service: The publish subscribe service to delete the node from. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} """ request = PubSubRequest('delete') request.recipient = service request.nodeIdentifier = nodeIdentifier request.sender = sender return request.send(self.xmlstream) def subscribe(self, service, nodeIdentifier, subscriber, options=None, sender=None): """ Subscribe to a publish subscribe node. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param subscriber: The entity to subscribe to the node. This entity will get notifications of new published items. @type subscriber: L{JID} @param options: Subscription options. @type options: C{dict} @return: Deferred that fires with L{Subscription} or errbacks with L{SubscriptionPending} or L{SubscriptionUnconfigured}. @rtype: L{defer.Deferred} """ request = PubSubRequest('subscribe') request.recipient = service request.nodeIdentifier = nodeIdentifier request.subscriber = subscriber request.sender = sender if options: form = data_form.Form(formType='submit', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) form.makeFields(options) request.options = form def cb(iq): subscription = Subscription.fromElement(iq.pubsub.subscription) if subscription.state == 'pending': raise SubscriptionPending() elif subscription.state == 'unconfigured': raise SubscriptionUnconfigured() else: # we assume subscription == 'subscribed' # any other value would be invalid, but that should have # yielded a stanza error. return subscription d = request.send(self.xmlstream) d.addCallback(cb) return d def unsubscribe(self, service, nodeIdentifier, subscriber, subscriptionIdentifier=None, sender=None): """ Unsubscribe from a publish subscribe node. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param subscriber: The entity to unsubscribe from the node. @type subscriber: L{JID} @param subscriptionIdentifier: Optional subscription identifier. @type subscriptionIdentifier: C{unicode} """ request = PubSubRequest('unsubscribe') request.recipient = service request.nodeIdentifier = nodeIdentifier request.subscriber = subscriber request.subscriptionIdentifier = subscriptionIdentifier request.sender = sender return request.send(self.xmlstream) def publish(self, service, nodeIdentifier, items=None, sender=None): """ Publish to a publish subscribe node. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param items: Optional list of L{Item}s to publish. @type items: C{list} """ request = PubSubRequest('publish') request.recipient = service request.nodeIdentifier = nodeIdentifier request.items = items request.sender = sender return request.send(self.xmlstream) def items(self, service, nodeIdentifier, maxItems=None, subscriptionIdentifier=None, sender=None): """ Retrieve previously published items from a publish subscribe node. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param maxItems: Optional limit on the number of retrieved items. @type maxItems: C{int} @param subscriptionIdentifier: Optional subscription identifier. In case the node has been subscribed to multiple times, this narrows the results to the specific subscription. @type subscriptionIdentifier: C{unicode} """ request = PubSubRequest('items') request.recipient = service request.nodeIdentifier = nodeIdentifier if maxItems: request.maxItems = str(int(maxItems)) request.subscriptionIdentifier = subscriptionIdentifier request.sender = sender def cb(iq): items = [] for element in iq.pubsub.items.elements(): if element.uri == NS_PUBSUB and element.name == 'item': items.append(element) return items d = request.send(self.xmlstream) d.addCallback(cb) return d def getOptions(self, service, nodeIdentifier, subscriber, subscriptionIdentifier=None, sender=None): """ Get subscription options. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param subscriber: The entity subscribed to the node. @type subscriber: L{JID} @param subscriptionIdentifier: Optional subscription identifier. @type subscriptionIdentifier: C{unicode} @rtype: L{data_form.Form} """ request = PubSubRequest('optionsGet') request.recipient = service request.nodeIdentifier = nodeIdentifier request.subscriber = subscriber request.subscriptionIdentifier = subscriptionIdentifier request.sender = sender def cb(iq): form = data_form.findForm(iq.pubsub.options, NS_PUBSUB_SUBSCRIBE_OPTIONS) form.typeCheck() return form d = request.send(self.xmlstream) d.addCallback(cb) return d def setOptions(self, service, nodeIdentifier, subscriber, options, subscriptionIdentifier=None, sender=None): """ Set subscription options. @param service: The publish subscribe service that keeps the node. @type service: L{JID} @param nodeIdentifier: The identifier of the node. @type nodeIdentifier: C{unicode} @param subscriber: The entity subscribed to the node. @type subscriber: L{JID} @param options: Subscription options. @type options: C{dict}. @param subscriptionIdentifier: Optional subscription identifier. @type subscriptionIdentifier: C{unicode} """ request = PubSubRequest('optionsSet') request.recipient = service request.nodeIdentifier = nodeIdentifier request.subscriber = subscriber request.subscriptionIdentifier = subscriptionIdentifier request.sender = sender form = data_form.Form(formType='submit', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS) form.makeFields(options) request.options = form d = request.send(self.xmlstream) return d class PubSubService(XMPPHandler, IQHandlerMixin): """ Protocol implementation for a XMPP Publish Subscribe Service. The word Service here is used as taken from the Publish Subscribe specification. It is the party responsible for keeping nodes and their subscriptions, and sending out notifications. Methods from the L{IPubSubService} interface that are called as a result of an XMPP request may raise exceptions. Alternatively the deferred returned by these methods may have their errback called. These are handled as follows: - If the exception is an instance of L{error.StanzaError}, an error response iq is returned. - Any other exception is reported using L{log.msg}. An error response with the condition C{internal-server-error} is returned. The default implementation of said methods raises an L{Unsupported} exception and are meant to be overridden. @ivar discoIdentity: Service discovery identity as a dictionary with keys C{'category'}, C{'type'} and C{'name'}. @ivar pubSubFeatures: List of supported publish-subscribe features for service discovery, as C{str}. @type pubSubFeatures: C{list} or C{None} """ implements(IPubSubService, disco.IDisco) iqHandlers = { '/*': '_onPubSubRequest', } _legacyHandlers = { 'publish': ('publish', ['sender', 'recipient', 'nodeIdentifier', 'items']), 'subscribe': ('subscribe', ['sender', 'recipient', 'nodeIdentifier', 'subscriber']), 'unsubscribe': ('unsubscribe', ['sender', 'recipient', 'nodeIdentifier', 'subscriber']), 'subscriptions': ('subscriptions', ['sender', 'recipient']), 'affiliations': ('affiliations', ['sender', 'recipient']), 'create': ('create', ['sender', 'recipient', 'nodeIdentifier']), 'getConfigurationOptions': ('getConfigurationOptions', []), 'default': ('getDefaultConfiguration', ['sender', 'recipient', 'nodeType']), 'configureGet': ('getConfiguration', ['sender', 'recipient', 'nodeIdentifier']), 'configureSet': ('setConfiguration', ['sender', 'recipient', 'nodeIdentifier', 'options']), 'items': ('items', ['sender', 'recipient', 'nodeIdentifier', 'maxItems', 'itemIdentifiers']), 'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier', 'itemIdentifiers']), 'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']), 'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']), } hideNodes = False def __init__(self, resource=None): self.resource = resource self.discoIdentity = {'category': 'pubsub', 'type': 'service', 'name': 'Generic Publish-Subscribe Service'} self.pubSubFeatures = [] def connectionMade(self): self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): def toInfo(nodeInfo): if not nodeInfo: return (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data'] info.append(disco.DiscoIdentity('pubsub', nodeType)) if metaData: form = data_form.Form(formType="result", formNamespace=NS_PUBSUB_META_DATA) form.addField( data_form.Field( var='pubsub#node_type', value=nodeType, label='The type of node (collection or leaf)' ) ) for metaDatum in metaData: form.addField(data_form.Field.fromDict(metaDatum)) info.append(form) return info = [] request = PubSubRequest('discoInfo') if self.resource is not None: resource = self.resource.locateResource(request) identity = resource.discoIdentity features = resource.features getInfo = resource.getInfo else: category = self.discoIdentity['category'] idType = self.discoIdentity['type'] name = self.discoIdentity['name'] identity = disco.DiscoIdentity(category, idType, name) features = self.pubSubFeatures getInfo = self.getNodeInfo if not nodeIdentifier: info.append(identity) info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS)) info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature)) for feature in features]) d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '') d.addCallback(toInfo) d.addErrback(log.err) d.addCallback(lambda _: info) return d def getDiscoItems(self, requestor, target, nodeIdentifier=''): if self.hideNodes: d = defer.succeed([]) elif self.resource is not None: request = PubSubRequest('discoInfo') resource = self.resource.locateResource(request) d = resource.getNodes(requestor, target, nodeIdentifier) elif nodeIdentifier: d = self.getNodes(requestor, target) else: d = defer.succeed([]) d.addCallback(lambda nodes: [disco.DiscoItem(target, node) for node in nodes]) return d def _onPubSubRequest(self, iq): request = PubSubRequest.fromElement(iq) if self.resource is not None: resource = self.resource.locateResource(request) else: resource = self # Preprocess the request, knowing the handling resource try: preProcessor = getattr(self, '_preProcess_%s' % request.verb) except AttributeError: pass else: request = preProcessor(resource, request) if request is None: return defer.succeed(None) # Process the request itself, if resource is not self: try: handler = getattr(resource, request.verb) except AttributeError: text = "Request verb: %s" % request.verb return defer.fail(Unsupported('', text)) d = handler(request) else: try: handlerName, argNames = self._legacyHandlers[request.verb] except KeyError: text = "Request verb: %s" % request.verb return defer.fail(Unsupported('', text)) handler = getattr(self, handlerName) args = [getattr(request, arg) for arg in argNames] d = handler(*args) # If needed, translate the result into a response try: cb = getattr(self, '_toResponse_%s' % request.verb) except AttributeError: pass else: d.addCallback(cb, resource, request) return d def _toResponse_subscribe(self, result, resource, request): response = domish.Element((NS_PUBSUB, "pubsub")) response.addChild(result.toElement(NS_PUBSUB)) return response def _toResponse_subscriptions(self, result, resource, request): response = domish.Element((NS_PUBSUB, 'pubsub')) subscriptions = response.addElement('subscriptions') for subscription in result: subscriptions.addChild(subscription.toElement(NS_PUBSUB)) return response def _toResponse_affiliations(self, result, resource, request): response = domish.Element((NS_PUBSUB, 'pubsub')) affiliations = response.addElement('affiliations') for nodeIdentifier, affiliation in result: item = affiliations.addElement('affiliation') item['node'] = nodeIdentifier item['affiliation'] = affiliation return response def _toResponse_create(self, result, resource, request): if not request.nodeIdentifier or request.nodeIdentifier != result: response = domish.Element((NS_PUBSUB, 'pubsub')) create = response.addElement('create') create['node'] = result return response else: return None def _formFromConfiguration(self, resource, values): fieldDefs = resource.getConfigurationOptions() form = data_form.Form(formType="form", formNamespace=NS_PUBSUB_NODE_CONFIG) form.makeFields(values, fieldDefs) return form def _checkConfiguration(self, resource, form): fieldDefs = resource.getConfigurationOptions() form.typeCheck(fieldDefs, filterUnknown=True) def _preProcess_create(self, resource, request): if request.options: self._checkConfiguration(resource, request.options) return request def _preProcess_default(self, resource, request): if request.nodeType not in ('leaf', 'collection'): raise error.StanzaError('not-acceptable') else: return request def _toResponse_default(self, options, resource, request): response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) default = response.addElement("default") form = self._formFromConfiguration(resource, options) default.addChild(form.toElement()) return response def _toResponse_configureGet(self, options, resource, request): response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) configure = response.addElement("configure") form = self._formFromConfiguration(resource, options) configure.addChild(form.toElement()) if request.nodeIdentifier: configure["node"] = request.nodeIdentifier return response def _preProcess_configureSet(self, resource, request): if request.options.formType == 'cancel': return None else: self._checkConfiguration(resource, request.options) return request def _toResponse_items(self, result, resource, request): response = domish.Element((NS_PUBSUB, 'pubsub')) items = response.addElement('items') items["node"] = request.nodeIdentifier for item in result: item.uri = NS_PUBSUB items.addChild(item) return response def _createNotification(self, eventType, service, nodeIdentifier, subscriber, subscriptions=None): headers = [] if subscriptions: for subscription in subscriptions: if nodeIdentifier != subscription.nodeIdentifier: headers.append(('Collection', subscription.nodeIdentifier)) message = domish.Element((None, "message")) message["from"] = service.full() message["to"] = subscriber.full() event = message.addElement((NS_PUBSUB_EVENT, "event")) element = event.addElement(eventType) element["node"] = nodeIdentifier if headers: message.addChild(shim.Headers(headers)) return message def _toResponse_affiliationsGet(self, result, resource, request): response = domish.Element((NS_PUBSUB_OWNER, 'pubsub')) affiliations = response.addElement('affiliations') if request.nodeIdentifier: affiliations['node'] = request.nodeIdentifier for entity, affiliation in result.iteritems(): item = affiliations.addElement('affiliation') item['jid'] = entity.full() item['affiliation'] = affiliation return response # public methods def notifyPublish(self, service, nodeIdentifier, notifications): for subscriber, subscriptions, items in notifications: message = self._createNotification('items', service, nodeIdentifier, subscriber, subscriptions) for item in items: item.uri = NS_PUBSUB_EVENT message.event.items.addChild(item) self.send(message) def notifyDelete(self, service, nodeIdentifier, subscribers, redirectURI=None): for subscriber in subscribers: message = self._createNotification('delete', service, nodeIdentifier, subscriber) if redirectURI: redirect = message.event.delete.addElement('redirect') redirect['uri'] = redirectURI self.send(message) def getNodeInfo(self, requestor, service, nodeIdentifier): return None def getNodes(self, requestor, service): return [] def publish(self, requestor, service, nodeIdentifier, items): raise Unsupported('publish') def subscribe(self, requestor, service, nodeIdentifier, subscriber): raise Unsupported('subscribe') def unsubscribe(self, requestor, service, nodeIdentifier, subscriber): raise Unsupported('subscribe') def subscriptions(self, requestor, service): raise Unsupported('retrieve-subscriptions') def affiliations(self, requestor, service): raise Unsupported('retrieve-affiliations') def create(self, requestor, service, nodeIdentifier): raise Unsupported('create-nodes') def getConfigurationOptions(self): return {} def getDefaultConfiguration(self, requestor, service, nodeType): raise Unsupported('retrieve-default') def getConfiguration(self, requestor, service, nodeIdentifier): raise Unsupported('config-node') def setConfiguration(self, requestor, service, nodeIdentifier, options): raise Unsupported('config-node') def items(self, requestor, service, nodeIdentifier, maxItems, itemIdentifiers): raise Unsupported('retrieve-items') def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): raise Unsupported('retract-items') def purge(self, requestor, service, nodeIdentifier): raise Unsupported('purge-nodes') def delete(self, requestor, service, nodeIdentifier): raise Unsupported('delete-nodes') class PubSubResource(object): implements(IPubSubResource) features = [] discoIdentity = disco.DiscoIdentity('pubsub', 'service', 'Publish-Subscribe Service') def locateResource(self, request): return self def getInfo(self, requestor, service, nodeIdentifier): return defer.succeed(None) def getNodes(self, requestor, service, nodeIdentifier): return defer.succeed([]) def getConfigurationOptions(self): return {} def publish(self, request): return defer.fail(Unsupported('publish')) def subscribe(self, request): return defer.fail(Unsupported('subscribe')) def unsubscribe(self, request): return defer.fail(Unsupported('subscribe')) def subscriptions(self, request): return defer.fail(Unsupported('retrieve-subscriptions')) def affiliations(self, request): return defer.fail(Unsupported('retrieve-affiliations')) def create(self, request): return defer.fail(Unsupported('create-nodes')) def default(self, request): return defer.fail(Unsupported('retrieve-default')) def configureGet(self, request): return defer.fail(Unsupported('config-node')) def configureSet(self, request): return defer.fail(Unsupported('config-node')) def items(self, request): return defer.fail(Unsupported('retrieve-items')) def retract(self, request): return defer.fail(Unsupported('retract-items')) def purge(self, request): return defer.fail(Unsupported('purge-nodes')) def delete(self, request): return defer.fail(Unsupported('delete-nodes')) def affiliationsGet(self, request): return defer.fail(Unsupported('modify-affiliations')) def affiliationsSet(self, request): return defer.fail(Unsupported('modify-affiliations')) def subscriptionsGet(self, request): return defer.fail(Unsupported('manage-subscriptions')) def subscriptionsSet(self, request): return defer.fail(Unsupported('manage-subscriptions')) wokkel-0.7.1/wokkel/muc.py0000664000175000017500000013101011707274572016247 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_muc -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Multi-User Chat protocol. This protocol is specified in U{XEP-0045}. """ from dateutil.tz import tzutc from zope.interface import implements from twisted.internet import defer from twisted.words.protocols.jabber import jid, error, xmlstream from twisted.words.xish import domish from wokkel import data_form, generic, iwokkel, xmppim from wokkel.compat import Values, ValueConstant from wokkel.delay import Delay, DelayMixin from wokkel.subprotocols import XMPPHandler from wokkel.iwokkel import IMUCClient # Multi User Chat namespaces NS_MUC = 'http://jabber.org/protocol/muc' NS_MUC_USER = NS_MUC + '#user' NS_MUC_ADMIN = NS_MUC + '#admin' NS_MUC_OWNER = NS_MUC + '#owner' NS_MUC_ROOMINFO = NS_MUC + '#roominfo' NS_MUC_CONFIG = NS_MUC + '#roomconfig' NS_MUC_REQUEST = NS_MUC + '#request' NS_MUC_REGISTER = NS_MUC + '#register' NS_REGISTER = 'jabber:iq:register' MESSAGE = '/message' PRESENCE = '/presence' GROUPCHAT = MESSAGE +'[@type="groupchat"]' DEFER_TIMEOUT = 30 # basic timeout is 30 seconds class STATUS_CODE(Values): REALJID_PUBLIC = ValueConstant(100) AFFILIATION_CHANGED = ValueConstant(101) UNAVAILABLE_SHOWN = ValueConstant(102) UNAVAILABLE_NOT_SHOWN = ValueConstant(103) CONFIGURATION_CHANGED = ValueConstant(104) SELF_PRESENCE = ValueConstant(110) LOGGING_ENABLED = ValueConstant(170) LOGGING_DISABLED = ValueConstant(171) NON_ANONYMOUS = ValueConstant(172) SEMI_ANONYMOUS = ValueConstant(173) FULLY_ANONYMOUS = ValueConstant(174) ROOM_CREATED = ValueConstant(201) NICK_ASSIGNED = ValueConstant(210) BANNED = ValueConstant(301) NEW_NICK = ValueConstant(303) KICKED = ValueConstant(307) REMOVED_AFFILIATION = ValueConstant(321) REMOVED_MEMBERSHIP = ValueConstant(322) REMOVED_SHUTDOWN = ValueConstant(332) class Statuses(set): """ Container of MUC status conditions. This is currently implemented as a set of constant values from L{STATUS_CODE}. Instances of this class provide L{IMUCStatuses}, that defines the supported operations. Even though this class currently derives from C{set}, future versions might not. This provides an upgrade path to cater for extensible status conditions, as defined in U{XEP-0306}. """ implements(iwokkel.IMUCStatuses) class _FormRequest(generic.Request): """ Base class for form exchange requests. """ requestNamespace = None formNamespace = None def __init__(self, recipient, sender=None, options=None): if options is None: stanzaType = 'get' else: stanzaType = 'set' generic.Request.__init__(self, recipient, sender, stanzaType) self.options = options def toElement(self): element = generic.Request.toElement(self) query = element.addElement((self.requestNamespace, 'query')) if self.options is None: # This is a request for the configuration form. form = None elif self.options is False: form = data_form.Form(formType='cancel') else: form = data_form.Form(formType='submit', formNamespace=self.formNamespace) form.makeFields(self.options) if form is not None: query.addChild(form.toElement()) return element class ConfigureRequest(_FormRequest): """ Configure MUC room request. http://xmpp.org/extensions/xep-0045.html#roomconfig """ requestNamespace = NS_MUC_OWNER formNamespace = NS_MUC_CONFIG class RegisterRequest(_FormRequest): """ Register request. http://xmpp.org/extensions/xep-0045.html#register """ requestNamespace = NS_REGISTER formNamespace = NS_MUC_REGISTER class AdminItem(object): """ Item representing role and/or affiliation for admin request. """ def __init__(self, affiliation=None, role=None, entity=None, nick=None, reason=None): self.affiliation = affiliation self.role = role self.entity = entity self.nick = nick self.reason = reason def toElement(self): element = domish.Element((NS_MUC_ADMIN, 'item')) if self.entity: element['jid'] = self.entity.full() if self.nick: element['nick'] = self.nick if self.affiliation: element['affiliation'] = self.affiliation if self.role: element['role'] = self.role if self.reason: element.addElement('reason', content=self.reason) return element @classmethod def fromElement(Class, element): item = Class() if element.hasAttribute('jid'): item.entity = jid.JID(element['jid']) item.nick = element.getAttribute('nick') item.affiliation = element.getAttribute('affiliation') item.role = element.getAttribute('role') for child in element.elements(NS_MUC_ADMIN, 'reason'): item.reason = unicode(child) return item class AdminStanza(generic.Request): """ An admin request or response. """ childParsers = {(NS_MUC_ADMIN, 'query'): '_childParser_query'} def toElement(self): element = generic.Request.toElement(self) element.addElement((NS_MUC_ADMIN, 'query')) if self.items: for item in self.items: element.query.addChild(item.toElement()) return element def _childParser_query(self, element): self.items = [] for child in element.elements(NS_MUC_ADMIN, 'item'): self.items.append(AdminItem.fromElement(child)) class DestructionRequest(generic.Request): """ Room destruction request. @param reason: Optional reason for the destruction of this room. @type reason: C{unicode}. @param alternate: Optional room JID of an alternate venue. @type alternate: L{JID} @param password: Optional password for entering the alternate venue. @type password: C{unicode} """ stanzaType = 'set' def __init__(self, recipient, sender=None, reason=None, alternate=None, password=None): generic.Request.__init__(self, recipient, sender) self.reason = reason self.alternate = alternate self.password = password def toElement(self): element = generic.Request.toElement(self) element.addElement((NS_MUC_OWNER, 'query')) element.query.addElement('destroy') if self.alternate: element.query.destroy['jid'] = self.alternate.full() if self.password: element.query.destroy.addElement('password', content=self.password) if self.reason: element.query.destroy.addElement('reason', content=self.reason) return element class GroupChat(xmppim.Message, DelayMixin): """ A groupchat message. """ stanzaType = 'groupchat' def toElement(self, legacyDelay=False): """ Render into a domish Element. @param legacyDelay: If C{True} send the delayed delivery information in legacy format. """ element = xmppim.Message.toElement(self) if self.delay: element.addChild(self.delay.toElement(legacy=legacyDelay)) return element class PrivateChat(xmppim.Message): """ A chat message. """ stanzaType = 'chat' class InviteMessage(xmppim.Message): def __init__(self, recipient=None, sender=None, invitee=None, reason=None): xmppim.Message.__init__(self, recipient, sender) self.invitee = invitee self.reason = reason def toElement(self): element = xmppim.Message.toElement(self) child = element.addElement((NS_MUC_USER, 'x')) child.addElement('invite') child.invite['to'] = self.invitee.full() if self.reason: child.invite.addElement('reason', content=self.reason) return element class HistoryOptions(object): """ A history configuration object. @ivar maxchars: Limit the total number of characters in the history to "X" (where the character count is the characters of the complete XML stanzas, not only their XML character data). @type maxchars: C{int} @ivar maxstanzas: Limit the total number of messages in the history to "X". @type mazstanzas: C{int} @ivar seconds: Send only the messages received in the last "X" seconds. @type seconds: C{int} @ivar since: Send only the messages received since the datetime specified. Note that this must be an offset-aware instance. @type since: L{datetime.datetime} """ attributes = ['maxChars', 'maxStanzas', 'seconds', 'since'] def __init__(self, maxChars=None, maxStanzas=None, seconds=None, since=None): self.maxChars = maxChars self.maxStanzas = maxStanzas self.seconds = seconds self.since = since def toElement(self): """ Returns a L{domish.Element} representing the history options. """ element = domish.Element((NS_MUC, 'history')) for key in self.attributes: value = getattr(self, key, None) if value is not None: if key == 'since': stamp = value.astimezone(tzutc()) element[key] = stamp.strftime('%Y-%m-%dT%H:%M:%SZ') else: element[key.lower()] = str(value) return element class BasicPresence(xmppim.AvailabilityPresence): """ Availability presence sent from MUC client to service. @type history: L{HistoryOptions} """ history = None password = None def toElement(self): element = xmppim.AvailabilityPresence.toElement(self) muc = element.addElement((NS_MUC, 'x')) if self.password: muc.addElement('password', content=self.password) if self.history: muc.addChild(self.history.toElement()) return element class UserPresence(xmppim.AvailabilityPresence): """ Availability presence sent from MUC service to client. @ivar affiliation: Affiliation of the entity to the room. @type affiliation: C{unicode} @ivar role: Role of the entity in the room. @type role: C{unicode} @ivar entity: The real JID of the entity this presence is from. @type entity: L{JID} @ivar mucStatuses: Set of one or more status codes from L{STATUS_CODE}. See L{Statuses} for usage notes. @type mucStatuses: L{Statuses} @ivar nick: The nick name of the entity in the room. @type nick: C{unicode} """ affiliation = None role = None entity = None nick = None mucStatuses = None childParsers = {(NS_MUC_USER, 'x'): '_childParser_mucUser'} def __init__(self, *args, **kwargs): self.mucStatuses = Statuses() xmppim.AvailabilityPresence.__init__(self, *args, **kwargs) def _childParser_mucUser(self, element): """ Parse the MUC user extension element. """ for child in element.elements(): if child.uri != NS_MUC_USER: continue elif child.name == 'status': try: value = int(child.getAttribute('code')) statusCode = STATUS_CODE.lookupByValue(value) except (TypeError, ValueError): continue self.mucStatuses.add(statusCode) elif child.name == 'item': if child.hasAttribute('jid'): self.entity = jid.JID(child['jid']) self.nick = child.getAttribute('nick') self.affiliation = child.getAttribute('affiliation') self.role = child.getAttribute('role') for reason in child.elements(NS_MUC_ADMIN, 'reason'): self.reason = unicode(reason) # TODO: destroy class VoiceRequest(xmppim.Message): """ Voice request message. """ def toElement(self): element = xmppim.Message.toElement(self) # build data form form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST) form.addField(data_form.Field(var='muc#role', value='participant', label='Requested role')) element.addChild(form.toElement()) return element class MUCClientProtocol(xmppim.BasePresenceProtocol): """ Multi-User Chat client protocol. """ timeout = None presenceTypeParserMap = { 'error': generic.ErrorStanza, 'available': UserPresence, 'unavailable': UserPresence, } def __init__(self, reactor=None): XMPPHandler.__init__(self) if reactor: self._reactor = reactor else: from twisted.internet import reactor self._reactor = reactor def connectionInitialized(self): """ Called when the XML stream has been initialized. It initializes several XPath events to handle MUC stanzas that come in. """ xmppim.BasePresenceProtocol.connectionInitialized(self) self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat) self._roomOccupantMap = {} def _onGroupChat(self, element): """ A group chat message has been received from a MUC room. There are a few event methods that may get called here. L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}. """ message = GroupChat.fromElement(element) self.groupChatReceived(message) def groupChatReceived(self, message): """ Called when a groupchat message was received. This method is called with a parsed representation of a received groupchat message and can be overridden for further processing. For regular groupchat message, the C{body} attribute contains the message body. Conversation history sent by the room upon joining, will have the C{delay} attribute set, room subject changes the C{subject} attribute. See L{GroupChat} for details. @param message: Groupchat message. @type message: L{GroupChat} """ pass def _sendDeferred(self, stanza): """ Send presence stanza, adding a deferred with a timeout. @param stanza: The presence stanza to send over the wire. @type stanza: L{generic.Stanza} @param timeout: The number of seconds to wait before the deferred is timed out. @type timeout: C{int} The deferred object L{defer.Deferred} is returned. """ def onResponse(element): if element.getAttribute('type') == 'error': d.errback(error.exceptionFromStanza(element)) else: d.callback(UserPresence.fromElement(element)) def onTimeout(): d.errback(xmlstream.TimeoutError("Timeout waiting for response.")) def cancelTimeout(result): if call.active(): call.cancel() return result def recordOccupant(presence): occupantJID = presence.sender roomJID = occupantJID.userhostJID() self._roomOccupantMap[roomJID] = occupantJID return presence call = self._reactor.callLater(DEFER_TIMEOUT, onTimeout) d = defer.Deferred() d.addBoth(cancelTimeout) d.addCallback(recordOccupant) query = "/presence[@from='%s' or (@from='%s' and @type='error')]" % ( stanza.recipient.full(), stanza.recipient.userhost()) self.xmlstream.addOnetimeObserver(query, onResponse, priority=-1) self.xmlstream.send(stanza.toElement()) return d def join(self, roomJID, nick, historyOptions=None, password=None): """ Join a MUC room by sending presence to it. @param roomJID: The JID of the room the entity is joining. @type roomJID: L{JID} @param nick: The nick name for the entitity joining the room. @type nick: C{unicode} @param historyOptions: Options for conversation history sent by the room upon joining. @type historyOptions: L{HistoryOptions} @param password: Optional password for the room. @type password: C{unicode} @return: A deferred that fires when the entity is in the room or an error has occurred. """ occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick)) presence = BasicPresence(recipient=occupantJID) if password: presence.password = password if historyOptions: presence.history = historyOptions return self._sendDeferred(presence) def nick(self, roomJID, nick): """ Change an entity's nick name in a MUC room. See: http://xmpp.org/extensions/xep-0045.html#changenick @param roomJID: The JID of the room. @type roomJID: L{JID} @param nick: The new nick name within the room. @type nick: C{unicode} """ occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick)) presence = BasicPresence(recipient=occupantJID) return self._sendDeferred(presence) def status(self, roomJID, show=None, status=None): """ Change user status. See: http://xmpp.org/extensions/xep-0045.html#changepres @param roomJID: The Room JID of the room. @type roomJID: L{JID} @param show: The availability of the entity. Common values are xa, available, etc @type show: C{unicode} @param status: The current status of the entity. @type status: C{unicode} """ occupantJID = self._roomOccupantMap[roomJID] presence = BasicPresence(recipient=occupantJID, show=show, status=status) return self._sendDeferred(presence) def leave(self, roomJID): """ Leave a MUC room. See: http://xmpp.org/extensions/xep-0045.html#exit @param roomJID: The JID of the room. @type roomJID: L{JID} """ occupantJID = self._roomOccupantMap[roomJID] presence = xmppim.AvailabilityPresence(recipient=occupantJID, available=False) return self._sendDeferred(presence) def groupChat(self, roomJID, body): """ Send a groupchat message. """ message = GroupChat(recipient=roomJID, body=body) self.send(message.toElement()) def chat(self, occupantJID, body): """ Send a private chat message to a user in a MUC room. See: http://xmpp.org/extensions/xep-0045.html#privatemessage @param occupantJID: The Room JID of the other user. @type occupantJID: L{JID} """ message = PrivateChat(recipient=occupantJID, body=body) self.send(message.toElement()) def subject(self, roomJID, subject): """ Change the subject of a MUC room. See: http://xmpp.org/extensions/xep-0045.html#subject-mod @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param subject: The subject you want to set. @type subject: C{unicode} """ message = GroupChat(roomJID.userhostJID(), subject=subject) self.send(message.toElement()) def invite(self, roomJID, invitee, reason=None): """ Invite a xmpp entity to a MUC room. See: http://xmpp.org/extensions/xep-0045.html#invite @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param invitee: The entity that is being invited. @type invitee: L{JID} @param reason: The reason for the invite. @type reason: C{unicode} """ message = InviteMessage(recipient=roomJID, invitee=invitee, reason=reason) self.send(message.toElement()) def getRegisterForm(self, roomJID): """ Grab the registration form for a MUC room. @param room: The room jabber/xmpp entity id for the requested registration form. @type room: L{JID} """ def cb(response): form = data_form.findForm(response.query, NS_MUC_REGISTER) return form request = RegisterRequest(recipient=roomJID, options=None) d = self.request(request) d.addCallback(cb) return d def register(self, roomJID, options): """ Send a request to register for a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param options: A mapping of field names to values, or C{None} to cancel. @type options: C{dict} """ if options is None: options = False request = RegisterRequest(recipient=roomJID, options=options) return self.request(request) def voice(self, roomJID): """ Request voice for a moderated room. @param roomJID: The room jabber/xmpp entity id. @type roomJID: L{JID} """ message = VoiceRequest(recipient=roomJID) self.xmlstream.send(message.toElement()) def history(self, roomJID, messages): """ Send history to create a MUC based on a one on one chat. See: http://xmpp.org/extensions/xep-0045.html#continue @param roomJID: The room jabber/xmpp entity id. @type roomJID: L{JID} @param messages: The history to send to the room as an ordered list of message, represented by a dictionary with the keys C{'stanza'}, holding the original stanza a L{domish.Element}, and C{'timestamp'} with the timestamp. @type messages: C{list} of L{domish.Element} """ for message in messages: stanza = message['stanza'] stanza['type'] = 'groupchat' delay = Delay(stamp=message['timestamp']) sender = stanza.getAttribute('from') if sender is not None: delay.sender = jid.JID(sender) stanza.addChild(delay.toElement()) stanza['to'] = roomJID.userhost() if stanza.hasAttribute('from'): del stanza['from'] self.xmlstream.send(stanza) def getConfiguration(self, roomJID): """ Grab the configuration from the room. This sends an iq request to the room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @return: A deferred that fires with the room's configuration form as a L{data_form.Form} or C{None} if there are no configuration options available. """ def cb(response): form = data_form.findForm(response.query, NS_MUC_CONFIG) return form request = ConfigureRequest(recipient=roomJID, options=None) d = self.request(request) d.addCallback(cb) return d def configure(self, roomJID, options): """ Configure a room. @param roomJID: The room to configure. @type roomJID: L{JID} @param options: A mapping of field names to values, or C{None} to cancel. @type options: C{dict} """ if options is None: options = False request = ConfigureRequest(recipient=roomJID, options=options) return self.request(request) def _getAffiliationList(self, roomJID, affiliation): """ Send a request for an affiliation list in a room. """ def cb(response): stanza = AdminStanza.fromElement(response) return stanza.items request = AdminStanza(recipient=roomJID, stanzaType='get') request.items = [AdminItem(affiliation=affiliation)] d = self.request(request) d.addCallback(cb) return d def _getRoleList(self, roomJID, role): """ Send a request for a role list in a room. """ def cb(response): stanza = AdminStanza.fromElement(response) return stanza.items request = AdminStanza(recipient=roomJID, stanzaType='get') request.items = [AdminItem(role=role)] d = self.request(request) d.addCallback(cb) return d def getMemberList(self, roomJID): """ Get the member list of a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} """ return self._getAffiliationList(roomJID, 'member') def getAdminList(self, roomJID): """ Get the admin list of a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} """ return self._getAffiliationList(roomJID, 'admin') def getBanList(self, roomJID): """ Get an outcast list from a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} """ return self._getAffiliationList(roomJID, 'outcast') def getOwnerList(self, roomJID): """ Get an owner list from a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} """ return self._getAffiliationList(roomJID, 'owner') def getModeratorList(self, roomJID): """ Get the moderator list of a room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} """ d = self._getRoleList(roomJID, 'moderator') return d def _setAffiliation(self, roomJID, entity, affiliation, reason=None, sender=None): """ Send a request to change an entity's affiliation to a MUC room. """ request = AdminStanza(recipient=roomJID, sender=sender, stanzaType='set') item = AdminItem(entity=entity, affiliation=affiliation, reason=reason) request.items = [item] return self.request(request) def _setRole(self, roomJID, nick, role, reason=None, sender=None): """ Send a request to change an occupant's role in a MUC room. """ request = AdminStanza(recipient=roomJID, sender=sender, stanzaType='set') item = AdminItem(nick=nick, role=role, reason=reason) request.items = [item] return self.request(request) def modifyAffiliationList(self, roomJID, entities, affiliation, sender=None): """ Modify an affiliation list. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param entities: The list of entities to change for a room. @type entities: C{list} of L{JID} @param affiliation: The affilation to the entities will acquire. @type affiliation: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ request = AdminStanza(recipient=roomJID, sender=sender, stanzaType='set') request.items = [AdminItem(entity=entity, affiliation=affiliation) for entity in entities] return self.request(request) def grantVoice(self, roomJID, nick, reason=None, sender=None): """ Grant voice to an entity. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param nick: The nick name for the user in this room. @type nick: C{unicode} @param reason: The reason for granting voice to the entity. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ return self._setRole(roomJID, nick=nick, role='participant', reason=reason, sender=sender) def revokeVoice(self, roomJID, nick, reason=None, sender=None): """ Revoke voice from a participant. This will disallow the entity to send messages to a moderated room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param nick: The nick name for the user in this room. @type nick: C{unicode} @param reason: The reason for revoking voice from the entity. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ return self._setRole(roomJID, nick=nick, role='visitor', reason=reason, sender=sender) def grantModerator(self, roomJID, nick, reason=None, sender=None): """ Grant moderator privileges to a MUC room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param nick: The nick name for the user in this room. @type nick: C{unicode} @param reason: The reason for granting moderation to the entity. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ return self._setRole(roomJID, nick=nick, role='moderator', reason=reason, sender=sender) def ban(self, roomJID, entity, reason=None, sender=None): """ Ban a user from a MUC room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param entity: The bare JID of the entity to be banned. @type entity: L{JID} @param reason: The reason for banning the entity. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ return self._setAffiliation(roomJID, entity, 'outcast', reason=reason, sender=sender) def kick(self, roomJID, nick, reason=None, sender=None): """ Kick a user from a MUC room. @param roomJID: The bare JID of the room. @type roomJID: L{JID} @param nick: The occupant to be banned. @type nick: C{unicode} @param reason: The reason given for the kick. @type reason: C{unicode} @param sender: The entity sending the request. @type sender: L{JID} """ return self._setRole(roomJID, nick, 'none', reason=reason, sender=sender) def destroy(self, roomJID, reason=None, alternate=None, password=None): """ Destroy a room. @param roomJID: The JID of the room. @type roomJID: L{JID} @param reason: The reason for the destruction of the room. @type reason: C{unicode} @param alternate: The JID of the room suggested as an alternate venue. @type alternate: L{JID} """ request = DestructionRequest(recipient=roomJID, reason=reason, alternate=alternate, password=password) return self.request(request) class User(object): """ A user/entity in a multi-user chat room. """ def __init__(self, nick, entity=None): self.nick = nick self.entity = entity self.affiliation = 'none' self.role = 'none' self.status = None self.show = None class Room(object): """ A Multi User Chat Room. An in memory object representing a MUC room from the perspective of a client. @ivar roomJID: The Room JID of the MUC room. @type roomJID: L{JID} @ivar nick: The nick name for the client in this room. @type nick: C{unicode} @ivar occupantJID: The JID of the occupant in the room. Generated from roomJID and nick. @type occupantJID: L{JID} @ivar locked: Flag signalling a locked room. A locked room first needs to be configured before it can be used. See L{MUCClientProtocol.getConfiguration} and L{MUCClientProtocol.configure}. @type locked: C{bool} """ locked = False def __init__(self, roomJID, nick): """ Initialize the room. """ self.roomJID = roomJID self.setNick(nick) self.roster = {} def setNick(self, nick): self.occupantJID = jid.internJID(u"%s/%s" % (self.roomJID, nick)) self.nick = nick def addUser(self, user): """ Add a user to the room roster. @param user: The user object that is being added to the room. @type user: L{User} """ self.roster[user.nick] = user def inRoster(self, user): """ Check if a user is in the MUC room. @param user: The user object to check. @type user: L{User} """ return user.nick in self.roster def getUser(self, nick): """ Get a user from the room's roster. @param nick: The nick for the user in the MUC room. @type nick: C{unicode} """ return self.roster.get(nick) def removeUser(self, user): """ Remove a user from the MUC room's roster. @param user: The user object to check. @type user: L{User} """ if self.inRoster(user): del self.roster[user.nick] class MUCClient(MUCClientProtocol): """ Multi-User Chat client protocol. This is a subclass of L{XMPPHandler} and implements L{IMUCClient}. @ivar _rooms: Collection of occupied rooms, keyed by the bare JID of the room. Note that a particular entity can only join a room once at a time. @type _rooms: C{dict} """ implements(IMUCClient) def __init__(self, reactor=None): MUCClientProtocol.__init__(self, reactor) self._rooms = {} def _addRoom(self, room): """ Add a room to the room collection. Rooms are stored by the JID of the room itself. I.e. it uses the Room ID and service parts of the Room JID. @note: An entity can only join a particular room once. """ roomJID = room.occupantJID.userhostJID() self._rooms[roomJID] = room def _getRoom(self, roomJID): """ Grab a room from the room collection. This uses the Room ID and service parts of the given JID to look up the L{Room} instance associated with it. @type occupantJID: L{JID} """ return self._rooms.get(roomJID) def _removeRoom(self, roomJID): """ Delete a room from the room collection. """ if roomJID in self._rooms: del self._rooms[roomJID] def _getRoomUser(self, stanza): """ Lookup the room and user associated with the stanza's sender. """ occupantJID = stanza.sender if not occupantJID: return None, None # when a user leaves a room we need to update it room = self._getRoom(occupantJID.userhostJID()) if room is None: # not in the room yet return None, None # Check if user is in roster nick = occupantJID.resource user = room.getUser(nick) return room, user def unavailableReceived(self, presence): """ Unavailable presence was received. If this was received from a MUC room occupant JID, that occupant has left the room. """ room, user = self._getRoomUser(presence) if room is None or user is None: return room.removeUser(user) self.userLeftRoom(room, user) def availableReceived(self, presence): """ Available presence was received. """ room, user = self._getRoomUser(presence) if room is None: return if user is None: nick = presence.sender.resource user = User(nick, presence.entity) # Update user status user.status = presence.status user.show = presence.show if room.inRoster(user): self.userUpdatedStatus(room, user, presence.show, presence.status) else: room.addUser(user) self.userJoinedRoom(room, user) def groupChatReceived(self, message): """ A group chat message has been received from a MUC room. There are a few event methods that may get called here. L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}. """ room, user = self._getRoomUser(message) if room is None: return if message.subject: self.receivedSubject(room, user, message.subject) elif message.delay is None: self.receivedGroupChat(room, user, message) else: self.receivedHistory(room, user, message) def userJoinedRoom(self, room, user): """ User has joined a MUC room. This method will need to be modified inorder for clients to do something when this event occurs. @param room: The room the user has joined. @type room: L{Room} @param user: The user that joined the MUC room. @type user: L{User} """ pass def userLeftRoom(self, room, user): """ User has left a room. This method will need to be modified inorder for clients to do something when this event occurs. @param room: The room the user has joined. @type room: L{Room} @param user: The user that left the MUC room. @type user: L{User} """ pass def userUpdatedStatus(self, room, user, show, status): """ User Presence has been received. This method will need to be modified inorder for clients to do something when this event occurs. """ pass def receivedSubject(self, room, user, subject): """ A (new) room subject has been received. This method will need to be modified inorder for clients to do something when this event occurs. """ pass def receivedGroupChat(self, room, user, message): """ A groupchat message was received. @param room: The room the message was received from. @type room: L{Room} @param user: The user that sent the message, or C{None} if it was a message from the room itself. @type user: L{User} @param message: The message. @type message: L{GroupChat} """ pass def receivedHistory(self, room, user, message): """ A groupchat message from the room's discussion history was received. This is identical to L{receivedGroupChat}, with the delayed delivery information (timestamp and original sender) in C{message.delay}. For anonymous rooms, C{message.delay.sender} is the room's address. @param room: The room the message was received from. @type room: L{Room} @param user: The user that sent the message, or C{None} if it was a message from the room itself. @type user: L{User} @param message: The message. @type message: L{GroupChat} """ pass def join(self, roomJID, nick, historyOptions=None, password=None): """ Join a MUC room by sending presence to it. @param roomJID: The JID of the room the entity is joining. @type roomJID: L{JID} @param nick: The nick name for the entitity joining the room. @type nick: C{unicode} @param historyOptions: Options for conversation history sent by the room upon joining. @type historyOptions: L{HistoryOptions} @param password: Optional password for the room. @type password: C{unicode} @return: A deferred that fires with the room when the entity is in the room, or with a failure if an error has occurred. """ def cb(presence): """ We have presence that says we joined a room. """ if STATUS_CODE.ROOM_CREATED in presence.mucStatuses: room.locked = True return room def eb(failure): self._removeRoom(roomJID) return failure room = Room(roomJID, nick) self._addRoom(room) d = MUCClientProtocol.join(self, roomJID, nick, historyOptions, password) d.addCallbacks(cb, eb) return d def nick(self, roomJID, nick): """ Change an entity's nick name in a MUC room. See: http://xmpp.org/extensions/xep-0045.html#changenick @param roomJID: The JID of the room, i.e. without a resource. @type roomJID: L{JID} @param nick: The new nick name within the room. @type nick: C{unicode} """ def cb(presence): # Presence confirmation, change the nickname. room.setNick(nick) return room room = self._getRoom(roomJID) d = MUCClientProtocol.nick(self, roomJID, nick) d.addCallback(cb) return d def leave(self, roomJID): """ Leave a MUC room. See: http://xmpp.org/extensions/xep-0045.html#exit @param roomJID: The Room JID of the room to leave. @type roomJID: L{JID} """ def cb(presence): self._removeRoom(roomJID) d = MUCClientProtocol.leave(self, roomJID) d.addCallback(cb) return d def status(self, roomJID, show=None, status=None): """ Change user status. See: http://xmpp.org/extensions/xep-0045.html#changepres @param roomJID: The Room JID of the room. @type roomJID: L{JID} @param show: The availability of the entity. Common values are xa, available, etc @type show: C{unicode} @param status: The current status of the entity. @type status: C{unicode} """ room = self._getRoom(roomJID) d = MUCClientProtocol.status(self, roomJID, show, status) d.addCallback(lambda _: room) return d def destroy(self, roomJID, reason=None, alternate=None, password=None): """ Destroy a room. @param roomJID: The JID of the room. @type roomJID: L{JID} @param reason: The reason for the destruction of the room. @type reason: C{unicode} @param alternate: The JID of the room suggested as an alternate venue. @type alternate: L{JID} """ def destroyed(iq): self._removeRoom(roomJID) d = MUCClientProtocol.destroy(self, roomJID, reason, alternate) d.addCallback(destroyed) return d wokkel-0.7.1/wokkel/xmppim.py0000775000175000017500000010156712074336410017002 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_xmppim -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP IM protocol support. This module provides generic implementations for the protocols defined in U{RFC 6121} (XMPP IM). """ import warnings from twisted.internet import defer from twisted.words.protocols.jabber import error from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish from wokkel.generic import ErrorStanza, Stanza, Request from wokkel.subprotocols import IQHandlerMixin from wokkel.subprotocols import XMPPHandler NS_XML = 'http://www.w3.org/XML/1998/namespace' NS_ROSTER = 'jabber:iq:roster' XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER class Presence(domish.Element): def __init__(self, to=None, type=None): domish.Element.__init__(self, (None, "presence")) if type: self["type"] = type if to is not None: self["to"] = to.full() class AvailablePresence(Presence): def __init__(self, to=None, show=None, statuses=None, priority=0): Presence.__init__(self, to, type=None) if show in ['away', 'xa', 'chat', 'dnd']: self.addElement('show', content=show) if statuses is not None: for lang, status in statuses.iteritems(): s = self.addElement('status', content=status) if lang: s[(NS_XML, "lang")] = lang if priority != 0: self.addElement('priority', content=unicode(int(priority))) class UnavailablePresence(Presence): def __init__(self, to=None, statuses=None): Presence.__init__(self, to, type='unavailable') if statuses is not None: for lang, status in statuses.iteritems(): s = self.addElement('status', content=status) if lang: s[(NS_XML, "lang")] = lang class PresenceClientProtocol(XMPPHandler): def connectionInitialized(self): self.xmlstream.addObserver('/presence', self._onPresence) def _getStatuses(self, presence): statuses = {} for element in presence.elements(): if element.name == 'status': lang = element.getAttribute((NS_XML, 'lang')) text = unicode(element) statuses[lang] = text return statuses def _onPresence(self, presence): type = presence.getAttribute("type", "available") try: handler = getattr(self, '_onPresence%s' % (type.capitalize())) except AttributeError: return else: handler(presence) def _onPresenceAvailable(self, presence): entity = JID(presence["from"]) show = unicode(presence.show or '') if show not in ['away', 'xa', 'chat', 'dnd']: show = None statuses = self._getStatuses(presence) try: priority = int(unicode(presence.priority or '')) or 0 except ValueError: priority = 0 self.availableReceived(entity, show, statuses, priority) def _onPresenceUnavailable(self, presence): entity = JID(presence["from"]) statuses = self._getStatuses(presence) self.unavailableReceived(entity, statuses) def _onPresenceSubscribed(self, presence): self.subscribedReceived(JID(presence["from"])) def _onPresenceUnsubscribed(self, presence): self.unsubscribedReceived(JID(presence["from"])) def _onPresenceSubscribe(self, presence): self.subscribeReceived(JID(presence["from"])) def _onPresenceUnsubscribe(self, presence): self.unsubscribeReceived(JID(presence["from"])) def availableReceived(self, entity, show=None, statuses=None, priority=0): """ Available presence was received. @param entity: entity from which the presence was received. @type entity: {JID} @param show: detailed presence information. One of C{'away'}, C{'xa'}, C{'chat'}, C{'dnd'} or C{None}. @type show: C{str} or C{NoneType} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} @param priority: priority level of the resource. @type priority: C{int} """ def unavailableReceived(self, entity, statuses=None): """ Unavailable presence was received. @param entity: entity from which the presence was received. @type entity: {JID} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} """ def subscribedReceived(self, entity): """ Subscription approval confirmation was received. @param entity: entity from which the confirmation was received. @type entity: {JID} """ def unsubscribedReceived(self, entity): """ Unsubscription confirmation was received. @param entity: entity from which the confirmation was received. @type entity: {JID} """ def subscribeReceived(self, entity): """ Subscription request was received. @param entity: entity from which the request was received. @type entity: {JID} """ def unsubscribeReceived(self, entity): """ Unsubscription request was received. @param entity: entity from which the request was received. @type entity: {JID} """ def available(self, entity=None, show=None, statuses=None, priority=0): """ Send available presence. @param entity: optional entity to which the presence should be sent. @type entity: {JID} @param show: optional detailed presence information. One of C{'away'}, C{'xa'}, C{'chat'}, C{'dnd'}. @type show: C{str} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} @param priority: priority level of the resource. @type priority: C{int} """ self.send(AvailablePresence(entity, show, statuses, priority)) def unavailable(self, entity=None, statuses=None): """ Send unavailable presence. @param entity: optional entity to which the presence should be sent. @type entity: {JID} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} """ self.send(UnavailablePresence(entity, statuses)) def subscribe(self, entity): """ Send subscription request @param entity: entity to subscribe to. @type entity: {JID} """ self.send(Presence(to=entity, type='subscribe')) def unsubscribe(self, entity): """ Send unsubscription request @param entity: entity to unsubscribe from. @type entity: {JID} """ self.send(Presence(to=entity, type='unsubscribe')) def subscribed(self, entity): """ Send subscription confirmation. @param entity: entity that subscribed. @type entity: {JID} """ self.send(Presence(to=entity, type='subscribed')) def unsubscribed(self, entity): """ Send unsubscription confirmation. @param entity: entity that unsubscribed. @type entity: {JID} """ self.send(Presence(to=entity, type='unsubscribed')) class BasePresence(Stanza): """ Stanza of kind presence. """ stanzaKind = 'presence' class AvailabilityPresence(BasePresence): """ Presence. This represents availability presence (as opposed to L{SubscriptionPresence}). @ivar available: The availability being communicated. @type available: C{bool} @ivar show: More specific availability. Can be one of C{'chat'}, C{'away'}, C{'xa'}, C{'dnd'} or C{None}. @type show: C{str} or C{NoneType} @ivar statuses: Natural language texts to detail the (un)availability. These are represented as a mapping from language code (C{str} or C{None}) to the corresponding text (C{unicode}). If the key is C{None}, the associated text is in the default language. @type statuses: C{dict} @ivar priority: Priority level for this resource. Must be between -128 and 127. Defaults to 0. @type priority: C{int} """ childParsers = {(None, 'show'): '_childParser_show', (None, 'status'): '_childParser_status', (None, 'priority'): '_childParser_priority'} def __init__(self, recipient=None, sender=None, available=True, show=None, status=None, statuses=None, priority=0): BasePresence.__init__(self, recipient=recipient, sender=sender) self.available = available self.show = show self.statuses = statuses or {} if status: self.statuses[None] = status self.priority = priority def __get_status(self): if None in self.statuses: return self.statuses[None] elif self.statuses: for status in self.status.itervalues(): return status else: return None status = property(__get_status) def _childParser_show(self, element): show = unicode(element) if show in ('chat', 'away', 'xa', 'dnd'): self.show = show def _childParser_status(self, element): lang = element.getAttribute((NS_XML, 'lang'), None) text = unicode(element) self.statuses[lang] = text def _childParser_priority(self, element): try: self.priority = int(unicode(element)) except ValueError: pass def parseElement(self, element): BasePresence.parseElement(self, element) if self.stanzaType == 'unavailable': self.available = False def toElement(self): if not self.available: self.stanzaType = 'unavailable' presence = BasePresence.toElement(self) if self.available: if self.show in ('chat', 'away', 'xa', 'dnd'): presence.addElement('show', content=self.show) if self.priority != 0: presence.addElement('priority', content=unicode(self.priority)) for lang, text in self.statuses.iteritems(): status = presence.addElement('status', content=text) if lang: status[(NS_XML, 'lang')] = lang return presence class SubscriptionPresence(BasePresence): """ Presence subscription request or response. This kind of presence is used to represent requests for presence subscription and their replies. Based on L{BasePresence} and {Stanza}, it just uses the C{stanzaType} attribute to represent the type of subscription presence. This can be one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and C{'unsubscribed'}. """ class ProbePresence(BasePresence): """ Presence probe request. """ stanzaType = 'probe' class BasePresenceProtocol(XMPPHandler): """ XMPP Presence base protocol handler. This class is the base for protocol handlers that receive presence stanzas. Listening to all incoming presence stanzas, it extracts the stanza's type and looks up a matching stanza parser and calls the associated method. The method's name is the type + C{Received}. E.g. C{availableReceived}. See L{PresenceProtocol} for a complete example. @cvar presenceTypeParserMap: Maps presence stanza types to their respective stanza parser classes (derived from L{Stanza}). @type presenceTypeParserMap: C{dict} """ presenceTypeParserMap = {} def connectionInitialized(self): self.xmlstream.addObserver("/presence", self._onPresence) def _onPresence(self, element): """ Called when a presence stanza has been received. """ stanza = Stanza.fromElement(element) presenceType = stanza.stanzaType or 'available' try: parser = self.presenceTypeParserMap[presenceType] except KeyError: return presence = parser.fromElement(element) try: handler = getattr(self, '%sReceived' % presenceType) except AttributeError: return else: handler(presence) class PresenceProtocol(BasePresenceProtocol): presenceTypeParserMap = { 'error': ErrorStanza, 'available': AvailabilityPresence, 'unavailable': AvailabilityPresence, 'subscribe': SubscriptionPresence, 'unsubscribe': SubscriptionPresence, 'subscribed': SubscriptionPresence, 'unsubscribed': SubscriptionPresence, 'probe': ProbePresence, } def errorReceived(self, presence): """ Error presence was received. """ pass def availableReceived(self, presence): """ Available presence was received. """ pass def unavailableReceived(self, presence): """ Unavailable presence was received. """ pass def subscribedReceived(self, presence): """ Subscription approval confirmation was received. """ pass def unsubscribedReceived(self, presence): """ Unsubscription confirmation was received. """ pass def subscribeReceived(self, presence): """ Subscription request was received. """ pass def unsubscribeReceived(self, presence): """ Unsubscription request was received. """ pass def probeReceived(self, presence): """ Probe presence was received. """ pass def available(self, recipient=None, show=None, statuses=None, priority=0, status=None, sender=None): """ Send available presence. @param recipient: Optional Recipient to which the presence should be sent. @type recipient: {JID} @param show: Optional detailed presence information. One of C{'away'}, C{'xa'}, C{'chat'}, C{'dnd'}. @type show: C{str} @param statuses: Mapping of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} @param priority: priority level of the resource. @type priority: C{int} """ presence = AvailabilityPresence(recipient=recipient, sender=sender, show=show, statuses=statuses, status=status, priority=priority) self.send(presence.toElement()) def unavailable(self, recipient=None, statuses=None, sender=None): """ Send unavailable presence. @param recipient: Optional entity to which the presence should be sent. @type recipient: {JID} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} """ presence = AvailabilityPresence(recipient=recipient, sender=sender, available=False, statuses=statuses) self.send(presence.toElement()) def subscribe(self, recipient, sender=None): """ Send subscription request @param recipient: Entity to subscribe to. @type recipient: {JID} """ presence = SubscriptionPresence(recipient=recipient, sender=sender) presence.stanzaType = 'subscribe' self.send(presence.toElement()) def unsubscribe(self, recipient, sender=None): """ Send unsubscription request @param recipient: Entity to unsubscribe from. @type recipient: {JID} """ presence = SubscriptionPresence(recipient=recipient, sender=sender) presence.stanzaType = 'unsubscribe' self.send(presence.toElement()) def subscribed(self, recipient, sender=None): """ Send subscription confirmation. @param recipient: Entity that subscribed. @type recipient: {JID} """ presence = SubscriptionPresence(recipient=recipient, sender=sender) presence.stanzaType = 'subscribed' self.send(presence.toElement()) def unsubscribed(self, recipient, sender=None): """ Send unsubscription confirmation. @param recipient: Entity that unsubscribed. @type recipient: {JID} """ presence = SubscriptionPresence(recipient=recipient, sender=sender) presence.stanzaType = 'unsubscribed' self.send(presence.toElement()) def probe(self, recipient, sender=None): """ Send presence probe. @param recipient: Entity to be probed. @type recipient: {JID} """ presence = ProbePresence(recipient=recipient, sender=sender) self.send(presence.toElement()) class RosterItem(object): """ Roster item. This represents one contact from an XMPP contact list known as roster. @ivar entity: The JID of the contact. @type entity: L{JID} @ivar name: The associated nickname for this contact. @type name: C{unicode} @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, the roster owner is subscribed to the presence information of the contact. @type subscriptionTo: C{bool} @ivar subscriptionFrom: Contact's subscription state. If C{True}, the contact is subscribed to the presence information of the roster owner. @type subscriptionFrom: C{bool} @ivar pendingOut: Whether the subscription request to this contact is pending. @type pendingOut: C{bool} @ivar groups: Set of groups this contact is categorized in. Groups are represented by an opaque identifier of type C{unicode}. @type groups: C{set} @ivar approved: Signals pre-approved subscription. @type approved: C{bool} @ivar remove: Signals roster item removal. @type remove: C{bool} """ __subscriptionStates = {(False, False): None, (True, False): 'to', (False, True): 'from', (True, True): 'both'} def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False, name=u'', groups=None): self.entity = entity self.subscriptionTo = subscriptionTo self.subscriptionFrom = subscriptionFrom self.name = name self.groups = groups or set() self.pendingOut = False self.approved = False self.remove = False def __getJID(self): warnings.warn( "wokkel.xmppim.RosterItem.jid was deprecated in Wokkel 0.7.1; " "please use RosterItem.entity instead.", DeprecationWarning) return self.entity def __setJID(self, value): warnings.warn( "wokkel.xmppim.RosterItem.jid was deprecated in Wokkel 0.7.1; " "please use RosterItem.entity instead.", DeprecationWarning) self.entity = value jid = property(__getJID, __setJID, doc="JID of the contact. " "Deprecated in Wokkel 0.7.1; " "please use C{entity} instead.") def __getAsk(self): warnings.warn( "wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; " "please use RosterItem.pendingOut instead.", DeprecationWarning) return self.pendingOut def __setAsk(self, value): warnings.warn( "wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; " "please use RosterItem.pendingOut instead.", DeprecationWarning) self.pendingOut = value ask = property(__getAsk, __setAsk, doc="Pending out subscription. " "Deprecated in Wokkel 0.7.1; " "please use C{pendingOut} instead.") def toElement(self, rosterSet=False): """ Render to a DOM representation. If C{rosterSet} is set, some attributes, that may not be sent as a roster set, will not be rendered. @type rosterSet: C{boolean}. """ element = domish.Element((NS_ROSTER, 'item')) element['jid'] = self.entity.full() if self.remove: subscription = 'remove' else: if self.name: element['name'] = self.name if self.groups: for group in self.groups: element.addElement('group', content=group) if rosterSet: subscription = None else: subscription = self.__subscriptionStates[self.subscriptionTo, self.subscriptionFrom] if self.pendingOut: element['ask'] = u'subscribe' if self.approved: element['approved'] = u'true' if subscription: element['subscription'] = subscription return element @classmethod def fromElement(Class, element): entity = JID(element['jid']) item = Class(entity) subscription = element.getAttribute('subscription') if subscription == 'remove': item.remove = True else: item.name = element.getAttribute('name', u'') item.subscriptionTo = subscription in ('to', 'both') item.subscriptionFrom = subscription in ('from', 'both') item.pendingOut = element.getAttribute('ask') == 'subscribe' item.approved = element.getAttribute('approved') in ('true', '1') for subElement in domish.generateElementsQNamed(element.children, 'group', NS_ROSTER): item.groups.add(unicode(subElement)) return item class RosterRequest(Request): """ Roster request. @ivar item: Roster item to be set or pushed. @type item: L{RosterItem}. @ivar version: Roster version identifier for roster pushes and retrieving the roster as a delta from a known cached version. This should only be set if the recipient is known to support roster versioning. @type version: C{unicode} @ivar rosterSet: If set, this is a roster set request. This flag is used to make sure some attributes of the roster item are not rendered by L{toElement}. @type roster: C{boolean} """ item = None version = None rosterSet = False def parseRequest(self, element): self.version = element.getAttribute('ver') for child in element.elements(NS_ROSTER, 'item'): self.item = RosterItem.fromElement(child) break def toElement(self): element = Request.toElement(self) query = element.addElement((NS_ROSTER, 'query')) if self.version is not None: query['ver'] = self.version if self.item: query.addChild(self.item.toElement(rosterSet=self.rosterSet)) return element class RosterPushIgnored(Exception): """ Raised when this entity doesn't want to accept/trust a roster push. To avert presence leaks, a handler can raise L{RosterPushIgnored} when not accepting a roster push (directly or via Deferred). This will result in a C{'service-unavailable'} error being sent in return. """ class Roster(dict): """ In-memory roster container. This provides a roster as a mapping from L{JID} to L{RosterItem}. If roster versioning is used, the C{version} attribute holds the version identifier for this version of the roster. @ivar version: Roster version identifier. @type version: C{unicode}. """ version = None class RosterClientProtocol(XMPPHandler, IQHandlerMixin): """ Client side XMPP roster protocol. The roster can be retrieved using L{getRoster}. Subsequent changes to the roster will be pushed, resulting in calls to L{setReceived} or L{removeReceived}. These methods should be overridden to handle the roster pushes. RFC 6121 specifically allows entities other than a user's server to hold a roster for that user. However, how a client should deal with that is currently not yet specfied. By default roster pushes from other source. I.e. when C{request.sender} is set but the sender's bare JID is different from the user's bare JID. Set L{allowAnySender} to allow roster pushes from any sender. To avert presence leaks, L{RosterPushIgnored} should then be raised for pushes from untrusted senders. If roster versioning is supported by the server, the roster and subsequent pushes are annotated with a version identifier. This can be used to cache the roster on the client side. Upon reconnect, the client can request the roster with the version identifier of the cached version. The server may then choose to only send roster pushes for the changes since that version, instead of a complete roster. @cvar allowAnySender: Flag to allow roster pushes from any sender. C{False} by default. @type allowAnySender: C{boolean} """ allowAnySender = False iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"} def connectionInitialized(self): self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest) def getRoster(self, version=None): """ Retrieve contact list. The returned deferred fires with the result of the roster request as L{Roster}, a mapping from contact JID to L{RosterItem}. If roster versioning is supported, the recipient responds with either a the complete roster or with an empty result. In case of complete roster, the L{Roster} is annotated with a C{version} attribute that holds the version identifier for this version of the roster. This identifier should be used for caching. If the recipient responds with an empty result, the returned deferred fires with C{None}. This indicates that any roster modifications since C{version} will be sent as roster pushes. Note that the empty result (C{None}) is different from an empty roster (L{Roster} with no items). @param version: Optional version identifier of the last cashed version of the roster. This shall only be set if the recipient is known to support roster versioning. If there is no (valid) cached version of the roster, but roster versioning is desired, C{version} should be set to the empty string (C{u''}). @type version: C{unicode} @return: Roster as a mapping from L{JID} to L{RosterItem}. @rtype: L{twisted.internet.defer.Deferred} """ def processRoster(result): if result.query is not None: roster = Roster() roster.version = result.query.getAttribute('ver') for element in result.query.elements(NS_ROSTER, 'item'): item = RosterItem.fromElement(element) roster[item.entity] = item return roster else: return None request = RosterRequest(stanzaType='get') request.version = version d = self.request(request) d.addCallback(processRoster) return d def setItem(self, item): """ Add or modify a roster item. Note that RFC 6121 doesn't allow all properties of a roster item to be sent when setting a roster item. Only the C{name} and C{groups} attributes from C{item} are sent to the server. Presence subscription management must be done through L{PresenceProtocol}. @param item: The roster item to be set. @type item: L{RosterItem}. @rtype: L{twisted.internet.defer.Deferred} """ request = RosterRequest(stanzaType='set') request.rosterSet = True request.item = item return self.request(request) def removeItem(self, entity): """ Remove an item from the contact list. @param entity: The contact to remove the roster item for. @type entity: L{JID} @rtype: L{twisted.internet.defer.Deferred} """ item = RosterItem(entity) item.remove = True return self.setItem(item) def _onRosterSet(self, iq): def trapIgnored(failure): failure.trap(RosterPushIgnored) raise error.StanzaError('service-unavailable') request = RosterRequest.fromElement(iq) if (not self.allowAnySender and request.sender and request.sender.userhostJID() != self.parent.jid.userhostJID()): d = defer.fail(RosterPushIgnored()) elif request.item.remove: d = defer.maybeDeferred(self.removeReceived, request) else: d = defer.maybeDeferred(self.setReceived, request) d.addErrback(trapIgnored) return d def setReceived(self, request): """ Called when a roster push for a new or update item was received. @param request: The push request. @type request: L{RosterRequest} """ if hasattr(self, 'onRosterSet'): warnings.warn( "wokkel.xmppim.RosterClientProtocol.onRosterSet " "was deprecated in Wokkel 0.7.1; " "please use RosterClientProtocol.setReceived instead.", DeprecationWarning) return defer.maybeDeferred(self.onRosterSet, request.item) def removeReceived(self, request): """ Called when a roster push for the removal of an item was received. @param request: The push request. @type request: L{RosterRequest} """ if hasattr(self, 'onRosterRemove'): warnings.warn( "wokkel.xmppim.RosterClientProtocol.onRosterRemove " "was deprecated in Wokkel 0.7.1; " "please use RosterClientProtocol.removeReceived instead.", DeprecationWarning) return defer.maybeDeferred(self.onRosterRemove, request.item.entity) class Message(Stanza): """ A message stanza. """ stanzaKind = 'message' childParsers = { (None, 'body'): '_childParser_body', (None, 'subject'): '_childParser_subject', } def __init__(self, recipient=None, sender=None, body=None, subject=None): Stanza.__init__(self, recipient, sender) self.body = body self.subject = subject def _childParser_body(self, element): self.body = unicode(element) def _childParser_subject(self, element): self.subject = unicode(element) def toElement(self): element = Stanza.toElement(self) if self.body: element.addElement('body', content=self.body) if self.subject: element.addElement('subject', content=self.subject) return element class MessageProtocol(XMPPHandler): """ Generic XMPP subprotocol handler for incoming message stanzas. """ messageTypes = None, 'normal', 'chat', 'headline', 'groupchat' def connectionInitialized(self): self.xmlstream.addObserver("/message", self._onMessage) def _onMessage(self, message): if message.handled: return messageType = message.getAttribute("type") if messageType == 'error': return if messageType not in self.messageTypes: message["type"] = 'normal' self.onMessage(message) def onMessage(self, message): """ Called when a message stanza was received. """ wokkel-0.7.1/wokkel/data_form.py0000775000175000017500000005241511707274572017435 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_data_form -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ Data Forms. Support for Data Forms as described in U{XEP-0004}, along with support for Field Standardization for Data Forms as described in U{XEP-0068}. """ from zope.interface import implements from zope.interface.common import mapping from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish NS_X_DATA = 'jabber:x:data' class Error(Exception): """ Data Forms error. """ class FieldNameRequiredError(Error): """ A field name is required for this field type. """ class TooManyValuesError(Error): """ This field is single-value. """ class Option(object): """ Data Forms field option. @ivar value: Value of this option. @type value: C{unicode} @ivar label: Optional label for this option. @type label: C{unicode} or C{NoneType} """ def __init__(self, value, label=None): self.value = value self.label = label def __repr__(self): r = ["Option(", repr(self.value)] if self.label: r.append(", ") r.append(repr(self.label)) r.append(")") return u"".join(r) def toElement(self): """ Return the DOM representation of this option. @rtype: L{domish.Element}. """ option = domish.Element((NS_X_DATA, 'option')) option.addElement('value', content=self.value) if self.label: option['label'] = self.label return option @staticmethod def fromElement(element): valueElements = list(domish.generateElementsQNamed(element.children, 'value', NS_X_DATA)) if not valueElements: raise Error("Option has no value") label = element.getAttribute('label') return Option(unicode(valueElements[0]), label) class Field(object): """ Data Forms field. @ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'}, C{'hidden'}, C{'jid-multi'}, C{'jid-single'}, C{'list-multi'}, C{'list-single'}, C{'text-multi'}, C{'text-private'}, C{'text-single'}. The default is C{'text-single'}. @type fieldType: C{str} @ivar var: Field name. Optional if C{fieldType} is C{'fixed'}. @type var: C{str} @ivar label: Human readable label for this field. @type label: C{unicode} @ivar values: The values for this field, for multi-valued field types, as a list of C{bool}, C{unicode} or L{JID}. @type values: C{list} @ivar options: List of possible values to choose from in a response to this form as a list of L{Option}s. @type options: C{list} @ivar desc: Human readable description for this field. @type desc: C{unicode} @ivar required: Whether the field is required to be provided in a response to this form. @type required: C{bool} """ def __init__(self, fieldType='text-single', var=None, value=None, values=None, options=None, label=None, desc=None, required=False): """ Initialize this field. See the identically named instance variables for descriptions. If C{value} is not C{None}, it overrides C{values}, setting the given value as the only value for this field. """ self.fieldType = fieldType self.var = var if value is not None: self.value = value else: self.values = values or [] self.label = label try: self.options = [Option(value, label) for value, label in options.iteritems()] except AttributeError: self.options = options or [] self.desc = desc self.required = required def __repr__(self): r = ["Field(fieldType=", repr(self.fieldType)] if self.var: r.append(", var=") r.append(repr(self.var)) if self.label: r.append(", label=") r.append(repr(self.label)) if self.desc: r.append(", desc=") r.append(repr(self.desc)) if self.required: r.append(", required=") r.append(repr(self.required)) if self.values: r.append(", values=") r.append(repr(self.values)) if self.options: r.append(", options=") r.append(repr(self.options)) r.append(")") return u"".join(r) def __value_set(self, value): """ Setter of value property. Sets C{value} as the only element of L{values}. @type value: C{bool}, C{unicode} or L{JID} """ self.values = [value] def __value_get(self): """ Getter of value property. Returns the first element of L{values}, if present, or C{None}. """ if self.values: return self.values[0] else: return None value = property(__value_get, __value_set, doc=""" The value for this field, for single-valued field types. This is a special property accessing L{values}. Writing to this property empties L{values} and then sets the given value as the only element of L{values}. Reading from this propery returns the first element of L{values}. """) def typeCheck(self): """ Check field properties agains the set field type. """ if self.var is None and self.fieldType != 'fixed': raise FieldNameRequiredError() if self.values: if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi', 'text-multi', None) and len(self.values) > 1): raise TooManyValuesError() newValues = [] for value in self.values: if self.fieldType == 'boolean': if isinstance(value, (str, unicode)): checkValue = value.lower() if not checkValue in ('0', '1', 'false', 'true'): raise ValueError("Not a boolean") value = checkValue in ('1', 'true') value = bool(value) elif self.fieldType in ('jid-single', 'jid-multi'): if not hasattr(value, 'full'): value = JID(value) newValues.append(value) self.values = newValues def toElement(self, asForm=False): """ Return the DOM representation of this Field. @rtype: L{domish.Element}. """ self.typeCheck() field = domish.Element((NS_X_DATA, 'field')) if self.fieldType: field['type'] = self.fieldType if self.var is not None: field['var'] = self.var for value in self.values: if isinstance(value, bool): value = unicode(value).lower() else: value = unicode(value) field.addElement('value', content=value) if asForm: if self.fieldType in ('list-single', 'list-multi'): for option in self.options: field.addChild(option.toElement()) if self.label is not None: field['label'] = self.label if self.desc is not None: field.addElement('desc', content=self.desc) if self.required: field.addElement('required') return field @staticmethod def _parse_desc(field, element): desc = unicode(element) if desc: field.desc = desc @staticmethod def _parse_option(field, element): field.options.append(Option.fromElement(element)) @staticmethod def _parse_required(field, element): field.required = True @staticmethod def _parse_value(field, element): value = unicode(element) field.values.append(value) @staticmethod def fromElement(element): field = Field(None) for eAttr, fAttr in {'type': 'fieldType', 'var': 'var', 'label': 'label'}.iteritems(): value = element.getAttribute(eAttr) if value: setattr(field, fAttr, value) for child in element.elements(): if child.uri != NS_X_DATA: continue func = getattr(Field, '_parse_' + child.name, None) if func: func(field, child) return field @staticmethod def fromDict(fieldDict): """ Create a field from a dictionary. This is a short hand for passing arguments directly on Field object creation. The field type is represented by the C{'type'} key. For C{'options'} the value is not a list of L{Option}s, but a dictionary keyed by value, with an optional label as value. """ kwargs = fieldDict.copy() if 'type' in fieldDict: kwargs['fieldType'] = fieldDict['type'] del kwargs['type'] if 'options' in fieldDict: options = [] for value, label in fieldDict['options'].iteritems(): options.append(Option(value, label)) kwargs['options'] = options return Field(**kwargs) class Form(object): """ Data Form. There are two similarly named properties of forms. The C{formType} is the the so-called type of the form, and is set as the C{'type'} attribute on the form's root element. The Field Standardization specification in XEP-0068, defines a way to provide a context for the field names used in this form, by setting a special hidden field named C{'FORM_TYPE'}, to put the names of all other fields in the namespace of the value of that field. This namespace is recorded in the C{formNamespace} instance variable. A L{Form} also acts as read-only dictionary, with the values of fields keyed by their name. See L{__getitem__}. @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'}, or {'result'}. @type formType: C{str} @ivar title: Natural language title of the form. @type title: C{unicode} @ivar instructions: Natural language instructions as a list of C{unicode} strings without line breaks. @type instructions: C{list} @ivar formNamespace: The optional namespace of the field names for this form. This goes in the special field named C{'FORM_TYPE'}, if set. @type formNamespace: C{str} @ivar fields: Dictionary of named fields. Note that this is meant to be used for reading, only. One should use L{addField} or L{makeFields} and L{removeField} for adding and removing fields. @type fields: C{dict} @ivar fieldList: List of all fields, in the order they are added. Like C{fields}, this is meant to be used for reading, only. @type fieldList: C{list} """ implements(mapping.IIterableMapping, mapping.IEnumerableMapping, mapping.IReadMapping, mapping.IItemMapping) def __init__(self, formType, title=None, instructions=None, formNamespace=None, fields=None): self.formType = formType self.title = title self.instructions = instructions or [] self.formNamespace = formNamespace self.fieldList = [] self.fields = {} if fields: for field in fields: self.addField(field) def __repr__(self): r = ["Form(formType=", repr(self.formType)] if self.title: r.append(", title=") r.append(repr(self.title)) if self.instructions: r.append(", instructions=") r.append(repr(self.instructions)) if self.formNamespace: r.append(", formNamespace=") r.append(repr(self.formNamespace)) if self.fieldList: r.append(", fields=") r.append(repr(self.fieldList)) r.append(")") return u"".join(r) def addField(self, field): """ Add a field to this form. Fields are added in order, and C{fields} is a dictionary of the named fields, that is kept in sync only if this method is used for adding new fields. Multiple fields with the same name are disallowed. """ if field.var is not None: if field.var in self.fields: raise Error("Duplicate field %r" % field.var) self.fields[field.var] = field self.fieldList.append(field) def removeField(self, field): """ Remove a field from this form. """ self.fieldList.remove(field) if field.var is not None: del self.fields[field.var] def makeFields(self, values, fieldDefs=None, filterUnknown=True): """ Create fields from values and add them to this form. This creates fields from a mapping of name to value(s) and adds them to this form. It is typically used for generating outgoing forms. If C{fieldDefs} is not C{None}, this is used to fill in additional properties of fields, like the field types, labels and possible options. If C{filterUnknown} is C{True} and C{fieldDefs} is not C{None}, fields will only be created from C{values} with a corresponding entry in C{fieldDefs}. If the field type is unknown, the field type is C{None}. When the form is rendered using L{toElement}, these fields will have no C{'type'} attribute, and it is up to the receiving party to interpret the values properly (e.g. by knowing about the FORM_TYPE in C{formNamespace} and the field name). @param values: Values to create fields from. @type values: C{dict} @param fieldDefs: Field definitions as a dictionary. See L{wokkel.iwokkel.IPubSubService.getConfigurationOptions} @type fieldDefs: C{dict} @param filterUnknown: If C{True}, ignore fields that are not in C{fieldDefs}. @type filterUnknown: C{bool} """ for name, value in values.iteritems(): fieldDict = {'var': name, 'type': None} if fieldDefs is not None: if name in fieldDefs: fieldDict.update(fieldDefs[name]) elif filterUnknown: continue if isinstance(value, list): fieldDict['values'] = value else: fieldDict['value'] = value self.addField(Field.fromDict(fieldDict)) def toElement(self): """ Return the DOM representation of this Form. @rtype: L{domish.Element} """ form = domish.Element((NS_X_DATA, 'x')) form['type'] = self.formType if self.title: form.addElement('title', content=self.title) for instruction in self.instructions: form.addElement('instructions', content=instruction) if self.formNamespace is not None: field = Field('hidden', 'FORM_TYPE', self.formNamespace) form.addChild(field.toElement()) for field in self.fieldList: form.addChild(field.toElement(self.formType=='form')) return form @staticmethod def _parse_title(form, element): title = unicode(element) if title: form.title = title @staticmethod def _parse_instructions(form, element): instructions = unicode(element) if instructions: form.instructions.append(instructions) @staticmethod def _parse_field(form, element): field = Field.fromElement(element) if (field.var == "FORM_TYPE" and field.fieldType == 'hidden' and field.value): form.formNamespace = field.value else: form.addField(field) @staticmethod def fromElement(element): if (element.uri, element.name) != ((NS_X_DATA, 'x')): raise Error("Element provided is not a Data Form") form = Form(element.getAttribute("type")) for child in element.elements(): if child.uri != NS_X_DATA: continue func = getattr(Form, '_parse_' + child.name, None) if func: func(form, child) return form def __iter__(self): return iter(self.fields) def __len__(self): return len(self.fields) def __getitem__(self, key): """ Called to implement evaluation of self[key]. This returns the value of the field with the name in C{key}. For multi-value fields, the value is a list, otherwise a single value. If a field has no type, and the field has multiple values, the value of the list of values. Otherwise, it will be a single value. Raises C{KeyError} if there is no field with the name in C{key}. """ field = self.fields[key] if (field.fieldType in ('jid-multi', 'list-multi', 'text-multi') or (field.fieldType is None and len(field.values) > 1)): value = field.values else: value = field.value return value def get(self, key, default=None): try: return self[key] except KeyError: return default def __contains__(self, key): return key in self.fields def iterkeys(self): return iter(self) def itervalues(self): for key in self: yield self[key] def iteritems(self): for key in self: yield (key, self[key]) def keys(self): return list(self) def values(self): return list(self.itervalues()) def items(self): return list(self.iteritems()) def getValues(self): """ Extract values from the named form fields. For all named fields, the corresponding value or values are returned in a dictionary keyed by the field name. This is equivalent do C{dict(f)}, where C{f} is a L{Form}. @see: L{__getitem__} @rtype: C{dict} """ return dict(self) def typeCheck(self, fieldDefs=None, filterUnknown=False): """ Check values of fields according to the field definition. This method walks all named fields to check their values against their type, and is typically used for forms received from other entities. The field definition in C{fieldDefs} is used to check the field type. If C{filterUnknown} is C{True}, fields that are not present in C{fieldDefs} are removed from the form. If the field type is C{None} (when not set by the sending entity), the type from the field definitition is used, or C{'text-single'} if that is not set. If C{fieldDefs} is None, an empty dictionary is assumed. This is useful for coercing boolean and JID values on forms with type C{'form'}. @param fieldDefs: Field definitions as a dictionary. See L{wokkel.iwokkel.IPubSubService.getConfigurationOptions} @type fieldDefs: C{dict} @param filterUnknown: If C{True}, remove fields that are not in C{fieldDefs}. @type filterUnknown: C{bool} """ if fieldDefs is None: fieldDefs = {} filtered = [] for name, field in self.fields.iteritems(): if name in fieldDefs: fieldDef = fieldDefs[name] if 'type' not in fieldDef: fieldDef['type'] = 'text-single' if field.fieldType is None: field.fieldType = fieldDef['type'] elif field.fieldType != fieldDef['type']: raise TypeError("Field type for %r is %r, expected %r" % (name, field.fieldType, fieldDef['type'])) else: # Field type is correct pass field.typeCheck() elif filterUnknown: filtered.append(field) elif field.fieldType is not None: field.typeCheck() else: # Unknown field without type, no checking, no filtering pass for field in filtered: self.removeField(field) def findForm(element, formNamespace): """ Find a Data Form. Look for an element that represents a Data Form with the specified form namespace as a child element of the given element. """ if not element: return None for child in element.elements(): if (child.uri, child.name) == ((NS_X_DATA, 'x')): form = Form.fromElement(child) if (form.formNamespace == formNamespace or not form.formNamespace and form.formType=='cancel'): return form return None wokkel-0.7.1/wokkel/disco.py0000775000175000017500000004241711734574124016577 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_disco -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Service Discovery. The XMPP service discovery protocol is documented in U{XEP-0030}. """ from twisted.internet import defer from twisted.words.protocols.jabber import error, jid from twisted.words.xish import domish from wokkel import data_form, generic from wokkel.iwokkel import IDisco from wokkel.subprotocols import IQHandlerMixin, XMPPHandler NS_DISCO = 'http://jabber.org/protocol/disco' NS_DISCO_INFO = NS_DISCO + '#info' NS_DISCO_ITEMS = NS_DISCO + '#items' IQ_GET = '/iq[@type="get"]' DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]' DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]' class DiscoFeature(unicode): """ XMPP service discovery feature. This extends C{unicode} to convert to and from L{domish.Element}, but further behaves identically. """ def toElement(self): """ Render to a DOM representation. @rtype: L{domish.Element}. """ element = domish.Element((NS_DISCO_INFO, 'feature')) element['var'] = unicode(self) return element @staticmethod def fromElement(element): """ Parse a DOM representation into a L{DiscoFeature} instance. @param element: Element that represents the disco feature. @type element: L{domish.Element}. @rtype L{DiscoFeature}. """ featureURI = element.getAttribute('var', u'') feature = DiscoFeature(featureURI) return feature class DiscoIdentity(object): """ XMPP service discovery identity. @ivar category: The identity category. @type category: C{unicode} @ivar type: The identity type. @type type: C{unicode} @ivar name: The optional natural language name for this entity. @type name: C{unicode} """ def __init__(self, category, idType, name=None): self.category = category self.type = idType self.name = name def toElement(self): """ Generate a DOM representation. @rtype: L{domish.Element}. """ element = domish.Element((NS_DISCO_INFO, 'identity')) if self.category: element['category'] = self.category if self.type: element['type'] = self.type if self.name: element['name'] = self.name return element @staticmethod def fromElement(element): """ Parse a DOM representation into a L{DiscoIdentity} instance. @param element: Element that represents the disco identity. @type element: L{domish.Element}. @rtype L{DiscoIdentity}. """ category = element.getAttribute('category') idType = element.getAttribute('type') name = element.getAttribute('name') feature = DiscoIdentity(category, idType, name) return feature class DiscoInfo(object): """ XMPP service discovery info. @ivar nodeIdentifier: The optional node this info applies to. @type nodeIdentifier: C{unicode} @ivar features: Features as L{DiscoFeature}. @type features: C{set} @ivar identities: Identities as a mapping from (category, type) to name, all C{unicode}. @type identities: C{dict} @ivar extensions: Service discovery extensions as a mapping from the extension form's C{FORM_TYPE} (C{unicode}) to L{data_form.Form}. Forms with no C{FORM_TYPE} field are mapped as C{None}. Note that multiple forms with the same C{FORM_TYPE} have the last in sequence prevail. @type extensions: C{dict} @ivar _items: Sequence of added items. @type _items: C{list} """ def __init__(self): self.nodeIdentifier = '' self.features = set() self.identities = {} self.extensions = {} self._items = [] def __iter__(self): """ Iterator over sequence of items in the order added. """ return iter(self._items) def append(self, item): """ Add a piece of service discovery info. @param item: A feature, identity or extension form. @type item: L{DiscoFeature}, L{DiscoIdentity} or L{data_form.Form} """ self._items.append(item) if isinstance(item, DiscoFeature): self.features.add(item) elif isinstance(item, DiscoIdentity): self.identities[(item.category, item.type)] = item.name elif isinstance(item, data_form.Form): self.extensions[item.formNamespace] = item def toElement(self): """ Generate a DOM representation. This takes the items added with C{append} to create a DOM representation of service discovery information. @rtype: L{domish.Element}. """ element = domish.Element((NS_DISCO_INFO, 'query')) if self.nodeIdentifier: element['node'] = self.nodeIdentifier for item in self: element.addChild(item.toElement()) return element @staticmethod def fromElement(element): """ Parse a DOM representation into a L{DiscoInfo} instance. @param element: Element that represents the disco info. @type element: L{domish.Element}. @rtype L{DiscoInfo}. """ info = DiscoInfo() info.nodeIdentifier = element.getAttribute('node', '') for child in element.elements(): item = None if (child.uri, child.name) == (NS_DISCO_INFO, 'feature'): item = DiscoFeature.fromElement(child) elif (child.uri, child.name) == (NS_DISCO_INFO, 'identity'): item = DiscoIdentity.fromElement(child) elif (child.uri, child.name) == (data_form.NS_X_DATA, 'x'): item = data_form.Form.fromElement(child) if item is not None: info.append(item) return info class DiscoItem(object): """ XMPP service discovery item. @ivar entity: The entity holding the item. @type entity: L{jid.JID} @ivar nodeIdentifier: The optional node identifier for the item. @type nodeIdentifier: C{unicode} @ivar name: The optional natural language name for this entity. @type name: C{unicode} """ def __init__(self, entity, nodeIdentifier='', name=None): self.entity = entity self.nodeIdentifier = nodeIdentifier self.name = name def toElement(self): """ Generate a DOM representation. @rtype: L{domish.Element}. """ element = domish.Element((NS_DISCO_ITEMS, 'item')) if self.entity: element['jid'] = self.entity.full() if self.nodeIdentifier: element['node'] = self.nodeIdentifier if self.name: element['name'] = self.name return element @staticmethod def fromElement(element): """ Parse a DOM representation into a L{DiscoItem} instance. @param element: Element that represents the disco iitem. @type element: L{domish.Element}. @rtype L{DiscoItem}. """ try: entity = jid.JID(element.getAttribute('jid', ' ')) except jid.InvalidFormat: entity = None nodeIdentifier = element.getAttribute('node', '') name = element.getAttribute('name') feature = DiscoItem(entity, nodeIdentifier, name) return feature class DiscoItems(object): """ XMPP service discovery items. @ivar nodeIdentifier: The optional node this info applies to. @type nodeIdentifier: C{unicode} @ivar _items: Sequence of added items. @type _items: C{list} """ def __init__(self): self.nodeIdentifier = '' self._items = [] def __iter__(self): """ Iterator over sequence of items in the order added. """ return iter(self._items) def append(self, item): """ Append item to the sequence of items. @param item: Item to be added. @type item: L{DiscoItem} """ self._items.append(item) def toElement(self): """ Generate a DOM representation. This takes the items added with C{append} to create a DOM representation of service discovery items. @rtype: L{domish.Element}. """ element = domish.Element((NS_DISCO_ITEMS, 'query')) if self.nodeIdentifier: element['node'] = self.nodeIdentifier for item in self: element.addChild(item.toElement()) return element @staticmethod def fromElement(element): """ Parse a DOM representation into a L{DiscoItems} instance. @param element: Element that represents the disco items. @type element: L{domish.Element}. @rtype L{DiscoItems}. """ info = DiscoItems() info.nodeIdentifier = element.getAttribute('node', '') for child in element.elements(): if (child.uri, child.name) == (NS_DISCO_ITEMS, 'item'): item = DiscoItem.fromElement(child) info.append(item) return info class _DiscoRequest(generic.Request): """ A Service Discovery request. @ivar verb: Type of request: C{'info'} or C{'items'}. @type verb: C{str} @ivar nodeIdentifier: Optional node to request info for. @type nodeIdentifier: C{unicode} """ verb = None nodeIdentifier = '' _requestVerbMap = { NS_DISCO_INFO: 'info', NS_DISCO_ITEMS: 'items', } _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems())) def __init__(self, verb=None, nodeIdentifier='', recipient=None, sender=None): generic.Request.__init__(self, recipient=recipient, sender=sender, stanzaType='get') self.verb = verb self.nodeIdentifier = nodeIdentifier def parseElement(self, element): generic.Request.parseElement(self, element) verbElement = None for child in element.elements(): if child.name == 'query' and child.uri in self._requestVerbMap: self.verb = self._requestVerbMap[child.uri] verbElement = child if verbElement: self.nodeIdentifier = verbElement.getAttribute('node', '') def toElement(self): element = generic.Request.toElement(self) childURI = self._verbRequestMap[self.verb] query = element.addElement((childURI, 'query')) if self.nodeIdentifier: query['node'] = self.nodeIdentifier return element class DiscoClientProtocol(XMPPHandler): """ XMPP Service Discovery client protocol. """ def requestInfo(self, entity, nodeIdentifier='', sender=None): """ Request information discovery from a node. @param entity: Entity to send the request to. @type entity: L{jid.JID} @param nodeIdentifier: Optional node to request info from. @type nodeIdentifier: C{unicode} @param sender: Optional sender address. @type sender: L{jid.JID} """ request = _DiscoRequest('info', nodeIdentifier) request.sender = sender request.recipient = entity d = self.request(request) d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query)) return d def requestItems(self, entity, nodeIdentifier='', sender=None): """ Request items discovery from a node. @param entity: Entity to send the request to. @type entity: L{jid.JID} @param nodeIdentifier: Optional node to request info from. @type nodeIdentifier: C{unicode} @param sender: Optional sender address. @type sender: L{jid.JID} """ request = _DiscoRequest('items', nodeIdentifier) request.sender = sender request.recipient = entity d = self.request(request) d.addCallback(lambda iq: DiscoItems.fromElement(iq.query)) return d class DiscoHandler(XMPPHandler, IQHandlerMixin): """ Protocol implementation for XMPP Service Discovery. This handler will listen to XMPP service discovery requests and query the other handlers in C{parent} (see L{twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection}) for their identities, features and items according to L{IDisco}. """ iqHandlers = {DISCO_INFO: '_onDiscoInfo', DISCO_ITEMS: '_onDiscoItems'} def connectionInitialized(self): self.xmlstream.addObserver(DISCO_INFO, self.handleRequest) self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest) def _onDiscoInfo(self, iq): """ Called for incoming disco info requests. @param iq: The request iq element. @type iq: L{Element} """ request = _DiscoRequest.fromElement(iq) def toResponse(info): if request.nodeIdentifier and not info: raise error.StanzaError('item-not-found') else: response = DiscoInfo() response.nodeIdentifier = request.nodeIdentifier for item in info: response.append(item) return response.toElement() d = self.info(request.sender, request.recipient, request.nodeIdentifier) d.addCallback(toResponse) return d def _onDiscoItems(self, iq): """ Called for incoming disco items requests. @param iq: The request iq element. @type iq: L{Element} """ request = _DiscoRequest.fromElement(iq) def toResponse(items): response = DiscoItems() response.nodeIdentifier = request.nodeIdentifier for item in items: response.append(item) return response.toElement() d = self.items(request.sender, request.recipient, request.nodeIdentifier) d.addCallback(toResponse) return d def _gatherResults(self, deferredList): """ Gather results from a list of deferreds. Similar to L{defer.gatherResults}, but flattens the returned results, consumes errors after the first one and fires the errback of the returned deferred with the failure of the first deferred that fires its errback. @param deferredList: List of deferreds for which the results should be gathered. @type deferredList: C{list} @return: Deferred that fires with a list of gathered results. @rtype: L{defer.Deferred} """ def cb(resultList): results = [] for success, value in resultList: results.extend(value) return results def eb(failure): failure.trap(defer.FirstError) return failure.value.subFailure d = defer.DeferredList(deferredList, fireOnOneErrback=1, consumeErrors=1) d.addCallbacks(cb, eb) return d def info(self, requestor, target, nodeIdentifier): """ Inspect all sibling protocol handlers for disco info. Calls the L{getDiscoInfo} method on all child handlers of the parent, that provide L{IDisco}. @param requestor: The entity that sent the request. @type requestor: L{JID} @param target: The entity the request was sent to. @type target: L{JID} @param nodeIdentifier: The optional node being queried, or C{''}. @type nodeIdentifier: C{unicode} @return: Deferred with the gathered results from sibling handlers. @rtype: L{defer.Deferred} """ dl = [defer.maybeDeferred(handler.getDiscoInfo, requestor, target, nodeIdentifier) for handler in self.parent if IDisco.providedBy(handler)] return self._gatherResults(dl) def items(self, requestor, target, nodeIdentifier): """ Inspect all sibling protocol handlers for disco items. Calls the L{getDiscoItems} method on all child handlers of the parent, that provide L{IDisco}. @param requestor: The entity that sent the request. @type requestor: L{JID} @param target: The entity the request was sent to. @type target: L{JID} @param nodeIdentifier: The optional node being queried, or C{''}. @type nodeIdentifier: C{unicode} @return: Deferred with the gathered results from sibling handlers. @rtype: L{defer.Deferred} """ dl = [defer.maybeDeferred(handler.getDiscoItems, requestor, target, nodeIdentifier) for handler in self.parent if IDisco.providedBy(handler)] return self._gatherResults(dl) wokkel-0.7.1/wokkel/shim.py0000775000175000017500000000233411707274572016434 0ustar ralphmralphm00000000000000# -*- test-case-name: wokkel.test.test_shim -*- # # Copyright (c) Ralph Meijer. # See LICENSE for details. """ XMPP Stanza Headers and Internet Metadata. This protocol is specified in U{XEP-0131}. """ from twisted.words.xish import domish NS_SHIM = "http://jabber.org/protocol/shim" class Headers(domish.Element): def __init__(self, headers): domish.Element.__init__(self, (NS_SHIM, 'headers')) for name, value in headers: self.addElement('header', content=value)['name'] = name def extractHeaders(stanza): """ Extract SHIM headers from stanza. @param stanza: The stanza to extract headers from. @type stanza: L{Element} @return: Headers as a mapping from header name to a list of values. @rtype: C{dict} """ headers = {} for element in domish.generateElementsQNamed(stanza.children, 'headers', NS_SHIM): for header in domish.generateElementsQNamed(element.children, 'header', NS_SHIM): headers.setdefault(header['name'], []).append(unicode(header)) return headers wokkel-0.7.1/README0000664000175000017500000000217412074346026014475 0ustar ralphmralphm00000000000000Wokkel 0.7.1 What is this? ============= Wokkel is a Python module for experimenting with future enhancements to Twisted Words, that should eventually be included in the main Twisted main development tree. Some of the code in Wokkel has already made that transition, but is still included to be used with older Twisted releases. Requirements ============ - Python 2.4 or later. - Twisted 10.0.0 or later. - python-dateutil Resources ========= Wokkel's home is . Besides the general Twisted resources, help is available on the Twisted-Jabber mailing list:: Copyright and Warranty ====================== The code in this distribution is Copyright (c) Ralph Meijer, unless explicitly specified otherwise. Wokkel is made available under the MIT License. The included LICENSE file describes this in detail. Contributors ============ - Christopher Zorn - Jack Moffitt - Mike Malone - Pablo Martín - Fabio Forno - Kandaurov Oleg - Jérôme Poisson - Ilja Braude - Alexey Bezhan - Mayank Singh Author ====== Ralph Meijer