Changes in [127:1610e7a3b777:128:db5de9ee45fa]
- Files:
-
- 3 added
- 21 edited
Legend:
- Unmodified
- Added
- Removed
-
LICENSE
r19 r53 1 Copyright (c) 2003-200 8Ralph Meijer1 Copyright (c) 2003-2009 Ralph Meijer 2 2 3 3 Permission is hereby granted, free of charge, to any person obtaining -
NEWS
r31 r60 1 0.6.0 (2009-04-22) 2 ================== 3 4 Features 5 -------- 6 7 - Server-to-server support, based on the dialback protocol (#33). 8 - Enhancement to InternalProtocol to support multiple domains (#43). 9 - Publish-subscribe request abstraction (#45). 10 - Publish-subscribe abstraction to implement a node in code (#47). 11 - Enhancement to PubSubClient to send requests from a specific JID (#46). 12 13 Fixes 14 ----- 15 16 - Remove type interpretation in Data Forms field parsing code (#44). 17 18 19 0.5.0 (2009-04-07) 20 ================== 21 22 This release drops support for Twisted versions older than 8.0, including 23 Twisted 2.5 / Twisted Words 0.5. 24 25 Features 26 -------- 27 28 - Support for sending and receiving Publish-Subscribe node delete 29 notifications with redirect. 30 - Service Discovery client support, including an overhaul of disco data 31 classes (#28). 32 - Initial support for building XMPP servers has been added: 33 - XmlStreamServerFactory has been backported from Twisted Words (#29). 34 - An XMPP router has been added (#30). 35 - A server-side component authenticator has been added (#30). 36 - A new server-side component service, that connects to a router within the 37 same process, was added (#31). 38 39 40 Fixes 41 ----- 42 43 - Publish-Subscribe subscriptions requests work again (#22). 44 - Publish-Subscribe delete node requests now have the correct namespace (#27). 45 - NodeIDs in Service Discovery requests are now returned in responses (#7). 46 - The presence of stanzaType in toResponse is now checked correctly (#34). 47 - Data Form fields are now rendered depending on form type (#40). 48 - Data Form type checking issues were addressed (#41). 49 - Some compatibility fixes for Twisted 8.0 and 8.1. 50 - Various other fixes (#37, #42) and tracking changes to code already in 51 Twisted. 52 53 1 54 0.4.0 (2008-08-05) 2 55 ================== -
README
r31 r60 1 Wokkel 0. 4.01 Wokkel 0.6.0 2 2 3 3 What is this? 4 4 ============= 5 5 6 Wokkel is a Python module for experimenting with future enhancements 7 to Twisted Words. Everything is aimed to be included in the Twisted 8 main development tree eventually. 6 Wokkel is a Python module for experimenting with future enhancements to Twisted 7 Words, that should eventually be included in the main Twisted main development 8 tree. Some of the code in Wokkel has already made that transition, but is still 9 included to be used with older Twisted releases. 9 10 10 Dependencies 11 12 Requirements 11 13 ============ 12 14 13 This module depends on Twisted Words 0.5 or later. 15 - Python 2.4 or later. 16 - Twisted 8.0.0 or later. 14 17 15 Copyright 18 19 Resources 16 20 ========= 17 21 18 The code in this distribution is Copyright (c) 2003-2008 Ralph Meijer, unless 22 Wokkel's home is <http://wokkel.ik.nu/>. 23 24 Besides the general Twisted resources, help is available on the Twisted-Jabber 25 mailing list:: 26 27 <https://mailman.ik.nu/mailman/listinfo/twisted-jabber> 28 29 30 Copyright and Warranty 31 ====================== 32 33 The code in this distribution is Copyright (c) 2003-2009 Ralph Meijer, unless 19 34 excplicitely specified otherwise. 20 35 … … 22 37 describes this in detail. 23 38 24 Contact25 =======26 39 27 Questions, comments or suggestions are welcome! 40 Contributors 41 ============ 42 43 - Christopher Zorn 44 - Jack Moffitt 45 - Mike Malone 46 - Pablo Martin 47 48 49 Author 50 ====== 28 51 29 52 Ralph Meijer -
setup.py
r31 r60 1 1 #!/usr/bin/env python 2 2 3 # Copyright (c) 2003-200 8Ralph Meijer3 # Copyright (c) 2003-2009 Ralph Meijer 4 4 # See LICENSE for details. 5 5 … … 7 7 8 8 setup(name='wokkel', 9 version='0. 4.0',9 version='0.6.0', 10 10 description='Twisted Jabber support library', 11 11 author='Ralph Meijer', -
wokkel/client.py
r14 r55 12 12 13 13 from twisted.application import service 14 from twisted.internet import defer, protocol,reactor14 from twisted.internet import reactor 15 15 from twisted.names.srvconnect import SRVConnector 16 16 from twisted.words.protocols.jabber import client, sasl, xmlstream 17 17 18 try: 19 from twisted.words.xish.xmlstream import XmlStreamFactoryMixin 20 except ImportError: 21 from wokkel.compat import XmlStreamFactoryMixin 22 23 from wokkel.subprotocols import StreamManager, XMPPHandler 18 from wokkel import generic 19 from wokkel.subprotocols import StreamManager 24 20 25 21 class CheckAuthInitializer(object): … … 128 124 129 125 130 class DeferredClientFactory(XmlStreamFactoryMixin, protocol.ClientFactory): 131 protocol = xmlstream.XmlStream 126 class DeferredClientFactory(generic.DeferredXmlStreamFactory): 132 127 133 128 def __init__(self, jid, password): 134 self.authenticator = client.XMPPAuthenticator(jid, password)135 XmlStreamFactoryMixin.__init__(self, self.authenticator)129 authenticator = client.XMPPAuthenticator(jid, password) 130 generic.DeferredXmlStreamFactory.__init__(self, authenticator) 136 131 137 deferred = defer.Deferred()138 self.deferred = deferred139 140 self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)141 142 class ConnectionInitializedHandler(XMPPHandler):143 def connectionInitialized(self):144 deferred.callback(None)145 146 self.streamManager = StreamManager(self)147 self.addHandler(ConnectionInitializedHandler())148 149 def clientConnectionFailed(self, connector, reason):150 self.deferred.errback(reason)151 132 152 133 def addHandler(self, handler): … … 155 136 """ 156 137 self.streamManager.addHandler(handler) 138 157 139 158 140 def removeHandler(self, handler): … … 163 145 164 146 147 165 148 def clientCreator(factory): 166 149 domain = factory.authenticator.jid.host -
wokkel/compat.py
r8 r53 1 1 # -*- test-case-name: wokkel.test.test_compat -*- 2 2 # 3 # Copyright (c) 2001-200 7Twisted Matrix Laboratories.3 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. 4 4 # See LICENSE for details. 5 5 6 from twisted.internet import protocol 7 from twisted.words.protocols.jabber import xmlstream 6 8 from twisted.words.xish import domish 7 9 8 def toResponse(stanza, stanzaType=None):10 class BootstrapMixin(object): 9 11 """ 10 Create a response stanza from another stanza.12 XmlStream factory mixin to install bootstrap event observers. 11 13 12 This takes the addressing and id attributes from a stanza to create a (new, 13 empty) response stanza. The addressing attributes are swapped and the id 14 copied. Optionally, the stanza type of the response can be specified. 14 This mixin is for factories providing 15 L{IProtocolFactory<twisted.internet.interfaces.IProtocolFactory>} to make 16 sure bootstrap event observers are set up on protocols, before incoming 17 data is processed. Such protocols typically derive from 18 L{utility.EventDispatcher}, like L{XmlStream}. 15 19 16 @param stanza: the original stanza 17 @type stanza: L{domish.Element} 18 @param stanzaType: optional response stanza type 19 @type stanzaType: C{str} 20 @return: the response stanza. 21 @rtype: L{domish.Element} 20 You can set up bootstrap event observers using C{addBootstrap}. The 21 C{event} and C{fn} parameters correspond with the C{event} and 22 C{observerfn} arguments to L{utility.EventDispatcher.addObserver}. 23 24 @since: 8.2. 25 @ivar bootstraps: The list of registered bootstrap event observers. 26 @type bootstrap: C{list} 22 27 """ 23 28 24 toAddr = stanza.getAttribute('from') 25 fromAddr = stanza.getAttribute('to') 26 stanzaID = stanza.getAttribute('id') 27 28 response = domish.Element((None, stanza.name)) 29 if toAddr: 30 response['to'] = toAddr 31 if fromAddr: 32 response['from'] = fromAddr 33 if stanzaID: 34 response['id'] = stanzaID 35 if type: 36 response['type'] = stanzaType 37 38 return response 29 def __init__(self): 30 self.bootstraps = [] 39 31 40 32 41 class XmlStreamFactoryMixin(object): 33 def installBootstraps(self, dispatcher): 34 """ 35 Install registered bootstrap observers. 36 37 @param dispatcher: Event dispatcher to add the observers to. 38 @type dispatcher: L{utility.EventDispatcher} 39 """ 40 for event, fn in self.bootstraps: 41 dispatcher.addObserver(event, fn) 42 43 44 def addBootstrap(self, event, fn): 45 """ 46 Add a bootstrap event handler. 47 48 @param event: The event to register an observer for. 49 @type event: C{str} or L{xpath.XPathQuery} 50 @param fn: The observer callable to be registered. 51 """ 52 self.bootstraps.append((event, fn)) 53 54 55 def removeBootstrap(self, event, fn): 56 """ 57 Remove a bootstrap event handler. 58 59 @param event: The event the observer is registered for. 60 @type event: C{str} or L{xpath.XPathQuery} 61 @param fn: The registered observer callable. 62 """ 63 self.bootstraps.remove((event, fn)) 64 65 66 67 class XmlStreamServerFactory(BootstrapMixin, 68 protocol.ServerFactory): 42 69 """ 43 XmlStream factory mixin that takes care of event handlers.70 Factory for Jabber XmlStream objects as a server. 44 71 45 To make sure certain event observers are set up before incoming data is 46 processed, you can set up bootstrap event observers using C{addBootstrap}. 47 48 The C{event} and C{fn} parameters correspond with the C{event} and 49 C{observerfn} arguments to L{utility.EventDispatcher.addObserver}. 72 @since: 8.2. 73 @ivar authenticatorFactory: Factory callable that takes no arguments, to 74 create a fresh authenticator to be associated 75 with the XmlStream. 50 76 """ 51 77 52 def __init__(self, *args, **kwargs): 53 self.bootstraps = [] 54 self.args = args 55 self.kwargs = kwargs 78 protocol = xmlstream.XmlStream 79 80 def __init__(self, authenticatorFactory): 81 BootstrapMixin.__init__(self) 82 self.authenticatorFactory = authenticatorFactory 83 56 84 57 85 def buildProtocol(self, addr): … … 59 87 Create an instance of XmlStream. 60 88 61 The returned instance will have bootstrap event observers registered62 and will proceed to handle input on an incoming connection.89 A new authenticator instance will be created and passed to the new 90 XmlStream. Registered bootstrap event observers are installed as well. 63 91 """ 64 xs = self.protocol(*self.args, **self.kwargs) 92 authenticator = self.authenticatorFactory() 93 xs = self.protocol(authenticator) 65 94 xs.factory = self 66 for event, fn in self.bootstraps: 67 xs.addObserver(event, fn) 95 self.installBootstraps(xs) 68 96 return xs 69 70 def addBootstrap(self, event, fn):71 """72 Add a bootstrap event handler.73 """74 self.bootstraps.append((event, fn))75 76 def removeBootstrap(self, event, fn):77 """78 Remove a bootstrap event handler.79 """80 self.bootstraps.remove((event, fn)) -
wokkel/component.py
r6 r54 1 # Copyright (c) 2003-2007 Ralph Meijer 1 # -*- test-case-name: wokkel.test.test_component -*- 2 # 3 # Copyright (c) 2003-2008 Ralph Meijer 2 4 # See LICENSE for details. 3 5 … … 8 10 from twisted.application import service 9 11 from twisted.internet import reactor 10 from twisted.words.protocols.jabber import component 12 from twisted.python import log 13 from twisted.words.protocols.jabber.jid import internJID as JID 14 from twisted.words.protocols.jabber import component, error, xmlstream 11 15 from twisted.words.xish import domish 12 16 17 try: 18 #from twisted.words.protocols.jabber.xmlstream import XMPPHandler 19 from twisted.words.protocols.jabber.xmlstream import XMPPHandlerCollection 20 except ImportError: 21 #from wokkel.subprotocols import XMPPHandler 22 from wokkel.subprotocols import XMPPHandlerCollection 23 24 try: 25 from twisted.words.protocols.jabber.xmlstream import XmlStreamServerFactory 26 except ImportError: 27 from wokkel.compat import XmlStreamServerFactory 28 29 from wokkel.generic import XmlPipe 13 30 from wokkel.subprotocols import StreamManager 31 32 NS_COMPONENT_ACCEPT = 'jabber:component:accept' 14 33 15 34 class Component(StreamManager, service.Service): … … 57 76 def _getConnection(self): 58 77 return reactor.connectTCP(self.host, self.port, self.factory) 78 79 80 81 class InternalComponent(XMPPHandlerCollection, service.Service): 82 """ 83 Component service that connects directly to a router. 84 85 Instead of opening a socket to connect to a router, like L{Component}, 86 components of this type connect to a router in the same process. This 87 allows for one-process XMPP servers. 88 89 @ivar domains: Domains (as C{str}) this component will handle traffic for. 90 @type domains: L{set} 91 """ 92 93 def __init__(self, router, domain=None): 94 XMPPHandlerCollection.__init__(self) 95 96 self._router = router 97 self.domains = set() 98 if domain: 99 self.domains.add(domain) 100 101 self.xmlstream = None 102 103 def startService(self): 104 """ 105 Create a XML pipe, connect to the router and setup handlers. 106 """ 107 service.Service.startService(self) 108 109 self._pipe = XmlPipe() 110 self.xmlstream = self._pipe.source 111 112 for domain in self.domains: 113 self._router.addRoute(domain, self._pipe.sink) 114 115 for e in self: 116 e.makeConnection(self.xmlstream) 117 e.connectionInitialized() 118 119 120 def stopService(self): 121 """ 122 Disconnect from the router and handlers. 123 """ 124 service.Service.stopService(self) 125 126 for domain in self.domains: 127 self._router.removeRoute(domain, self._pipe.sink) 128 129 self._pipe = None 130 self.xmlstream = None 131 132 for e in self: 133 e.connectionLost(None) 134 135 136 def addHandler(self, handler): 137 """ 138 Add a new handler and connect it to the stream. 139 """ 140 XMPPHandlerCollection.addHandler(self, handler) 141 142 if self.xmlstream: 143 handler.makeConnection(self.xmlstream) 144 handler.connectionInitialized() 145 146 147 def send(self, obj): 148 """ 149 Send data to the XML stream, so it ends up at the router. 150 """ 151 self.xmlstream.send(obj) 152 153 154 155 class ListenComponentAuthenticator(xmlstream.ListenAuthenticator): 156 """ 157 Authenticator for accepting components. 158 159 @ivar secret: The shared used to authorized incoming component connections. 160 @type secret: C{str}. 161 """ 162 163 namespace = NS_COMPONENT_ACCEPT 164 165 def __init__(self, secret): 166 self.secret = secret 167 xmlstream.ListenAuthenticator.__init__(self) 168 169 170 def associateWithStream(self, xs): 171 """ 172 Associate the authenticator with a stream. 173 174 This sets the stream's version to 0.0, because the XEP-0114 component 175 protocol was not designed for XMPP 1.0. 176 """ 177 xs.version = (0, 0) 178 xmlstream.ListenAuthenticator.associateWithStream(self, xs) 179 180 181 def streamStarted(self, rootElement): 182 """ 183 Called by the stream when it has started. 184 185 This examines the default namespace of the incoming stream and whether 186 there is a requested hostname for the component. Then it generates a 187 stream identifier, sends a response header and adds an observer for 188 the first incoming element, triggering L{onElement}. 189 """ 190 191 xmlstream.ListenAuthenticator.streamStarted(self, rootElement) 192 193 # Compatibility fix for pre-8.2 implementations of ListenAuthenticator 194 if not self.xmlstream.sid: 195 from twisted.python import randbytes 196 self.xmlstream.sid = randbytes.secureRandom(8).encode('hex') 197 198 if rootElement.defaultUri != self.namespace: 199 exc = error.StreamError('invalid-namespace') 200 self.xmlstream.sendStreamError(exc) 201 return 202 203 # self.xmlstream.thisEntity is set to the address the component 204 # wants to assume. 205 if not self.xmlstream.thisEntity: 206 exc = error.StreamError('improper-addressing') 207 self.xmlstream.sendStreamError(exc) 208 return 209 210 self.xmlstream.sendHeader() 211 self.xmlstream.addOnetimeObserver('/*', self.onElement) 212 213 214 def onElement(self, element): 215 """ 216 Called on incoming XML Stanzas. 217 218 The very first element received should be a request for handshake. 219 Otherwise, the stream is dropped with a 'not-authorized' error. If a 220 handshake request was received, the hash is extracted and passed to 221 L{onHandshake}. 222 """ 223 if (element.uri, element.name) == (self.namespace, 'handshake'): 224 self.onHandshake(unicode(element)) 225 else: 226 exc = error.StreamError('not-authorized') 227 self.xmlstream.sendStreamError(exc) 228 229 230 def onHandshake(self, handshake): 231 """ 232 Called upon receiving the handshake request. 233 234 This checks that the given hash in C{handshake} is equal to a 235 calculated hash, responding with a handshake reply or a stream error. 236 If the handshake was ok, the stream is authorized, and XML Stanzas may 237 be exchanged. 238 """ 239 calculatedHash = xmlstream.hashPassword(self.xmlstream.sid, self.secret) 240 if handshake != calculatedHash: 241 exc = error.StreamError('not-authorized', text='Invalid hash') 242 self.xmlstream.sendStreamError(exc) 243 else: 244 self.xmlstream.send('<handshake/>') 245 self.xmlstream.dispatch(self.xmlstream, 246 xmlstream.STREAM_AUTHD_EVENT) 247 248 249 250 class Router(object): 251 """ 252 XMPP Server's Router. 253 254 A router connects the different components of the XMPP service and routes 255 messages between them based on the given routing table. 256 257 Connected components are trusted to have correct addressing in the 258 stanzas they offer for routing. 259 260 A route destination of C{None} adds a default route. Traffic for which no 261 specific route exists, will be routed to this default route. 262 263 @ivar routes: Routes based on the host part of JIDs. Maps host names to the 264 L{EventDispatcher<utility.EventDispatcher>}s that should 265 receive the traffic. A key of C{None} means the default 266 route. 267 @type routes: C{dict} 268 """ 269 270 def __init__(self): 271 self.routes = {} 272 273 274 def addRoute(self, destination, xs): 275 """ 276 Add a new route. 277 278 The passed XML Stream C{xs} will have an observer for all stanzas 279 added to route its outgoing traffic. In turn, traffic for 280 C{destination} will be passed to this stream. 281 282 @param destination: Destination of the route to be added as a host name 283 or C{None} for the default route. 284 @type destination: C{str} or C{NoneType}. 285 @param xs: XML Stream to register the route for. 286 @type xs: L{EventDispatcher<utility.EventDispatcher>}. 287 """ 288 self.routes[destination] = xs 289 xs.addObserver('/*', self.route) 290 291 292 def removeRoute(self, destination, xs): 293 """ 294 Remove a route. 295 296 @param destination: Destination of the route that should be removed. 297 @type destination: C{str}. 298 @param xs: XML Stream to remove the route for. 299 @type xs: L{EventDispatcher<utility.EventDispatcher>}. 300 """ 301 xs.removeObserver('/*', self.route) 302 if (xs == self.routes[destination]): 303 del self.routes[destination] 304 305 306 def route(self, stanza): 307 """ 308 Route a stanza. 309 310 @param stanza: The stanza to be routed. 311 @type stanza: L{domish.Element}. 312 """ 313 destination = JID(stanza['to']) 314 315 log.msg("Routing to %s: %r" % (destination.full(), stanza.toXml())) 316 317 if destination.host in self.routes: 318 self.routes[destination.host].send(stanza) 319 else: 320 self.routes[None].send(stanza) 321 322 323 324 class XMPPComponentServerFactory(XmlStreamServerFactory): 325 """ 326 XMPP Component Server factory. 327 328 This factory accepts XMPP external component connections and makes 329 the router service route traffic for a component's bound domain 330 to that component. 331 """ 332 333 logTraffic = False 334 335 def __init__(self, router, secret='secret'): 336 self.router = router 337 self.secret = secret 338 339 def authenticatorFactory(): 340 return ListenComponentAuthenticator(self.secret) 341 342 XmlStreamServerFactory.__init__(self, authenticatorFactory) 343 self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, 344 self.makeConnection) 345 self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, 346 self.connectionInitialized) 347 348 self.serial = 0 349 350 351 def makeConnection(self, xs): 352 """ 353 Called when a component connection was made. 354 355 This enables traffic debugging on incoming streams. 356 """ 357 xs.serial = self.serial 358 self.serial += 1 359 360 def logDataIn(buf): 361 log.msg("RECV (%d): %r" % (xs.serial, buf)) 362 363 def logDataOut(buf): 364 log.msg("SEND (%d): %r" % (xs.serial, buf)) 365 366 if self.logTraffic: 367 xs.rawDataInFn = logDataIn 368 xs.rawDataOutFn = logDataOut 369 370 xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) 371 372 373 def connectionInitialized(self, xs): 374 """ 375 Called when a component has succesfully authenticated. 376 377 Add the component to the routing table and establish a handler 378 for a closed connection. 379 """ 380 destination = xs.thisEntity.host 381 382 self.router.addRoute(destination, xs) 383 xs.addObserver(xmlstream.STREAM_END_EVENT, self.connectionLost, 0, 384 destination, xs) 385 386 387 def onError(self, reason): 388 log.err(reason, "Stream Error") 389 390 391 def connectionLost(self, destination, xs, reason): 392 self.router.removeRoute(destination, xs) -
wokkel/data_form.py
r29 r56 1 1 # -*- test-case-name: wokkel.test.test_data_form -*- 2 2 # 3 # Copyright (c) 2003-200 8Ralph Meijer3 # Copyright (c) 2003-2009 Ralph Meijer 4 4 # See LICENSE for details. 5 5 … … 69 69 Return the DOM representation of this option. 70 70 71 @rtype L{domish.Element}.71 @rtype: L{domish.Element}. 72 72 """ 73 73 option = domish.Element((NS_X_DATA, 'option')) … … 220 220 for value in self.values: 221 221 if self.fieldType == 'boolean': 222 # We send out the textual representation of boolean values 223 value = bool(int(value)) 222 if isinstance(value, (str, unicode)): 223 checkValue = value.lower() 224 if not checkValue in ('0', '1', 'false', 'true'): 225 raise ValueError("Not a boolean") 226 value = checkValue in ('1', 'true') 227 value = bool(value) 224 228 elif self.fieldType in ('jid-single', 'jid-multi'): 225 value = value.full() 229 if not hasattr(value, 'full'): 230 value = JID(value) 226 231 227 232 newValues.append(value) … … 229 234 self.values = newValues 230 235 231 def toElement(self ):236 def toElement(self, asForm=False): 232 237 """ 233 238 Return the DOM representation of this Field. 234 239 235 @rtype L{domish.Element}.240 @rtype: L{domish.Element}. 236 241 """ 237 242 … … 239 244 240 245 field = domish.Element((NS_X_DATA, 'field')) 241 field['type'] = self.fieldType 246 247 if asForm or self.fieldType != 'text-single': 248 field['type'] = self.fieldType 242 249 243 250 if self.var is not None: … … 247 254 if self.fieldType == 'boolean': 248 255 value = unicode(value).lower() 256 elif self.fieldType in ('jid-single', 'jid-multi'): 257 value = value.full() 258 249 259 field.addElement('value', content=value) 250 260 251 if self.fieldType in ('list-single', 'list-multi'): 252 for option in self.options: 253 field.addChild(option.toElement()) 254 255 if self.label is not None: 256 field['label'] = self.label 257 258 if self.desc is not None: 259 field.addElement('desc', content=self.desc) 260 261 if self.required: 262 field.addElement('required') 261 if asForm: 262 if self.fieldType in ('list-single', 'list-multi'): 263 for option in self.options: 264 field.addChild(option.toElement()) 265 266 if self.label is not None: 267 field['label'] = self.label 268 269 if self.desc is not None: 270 field.addElement('desc', content=self.desc) 271 272 if self.required: 273 field.addElement('required') 263 274 264 275 return field … … 285 296 def _parse_value(field, element): 286 297 value = unicode(element) 287 if field.fieldType == 'boolean':288 value = value.lower() in ('1', 'true')289 elif field.fieldType in ('jid-multi', 'jid-single'):290 value = JID(value)291 298 field.values.append(value) 292 299 … … 425 432 426 433 for field in self.fieldList: 427 form.addChild(field.toElement( ))434 form.addChild(field.toElement(self.formType=='form')) 428 435 429 436 return form -
wokkel/disco.py
r108 r128 1 1 # -*- test-case-name: wokkel.test.test_disco -*- 2 2 # 3 # Copyright (c) 2003-200 8Ralph Meijer3 # Copyright (c) 2003-2009 Ralph Meijer 4 4 # See LICENSE for details. 5 5 … … 15 15 from twisted.words.xish import domish 16 16 17 from wokkel import data_form 17 18 from wokkel.iwokkel import IDisco 18 19 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 19 20 20 NS = 'http://jabber.org/protocol/disco'21 NS_ INFO = NS+ '#info'22 NS_ ITEMS = NS+ '#items'21 NS_DISCO = 'http://jabber.org/protocol/disco' 22 NS_DISCO_INFO = NS_DISCO + '#info' 23 NS_DISCO_ITEMS = NS_DISCO + '#items' 23 24 24 25 IQ_GET = '/iq[@type="get"]' 25 DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_INFO + '"]' 26 DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_ITEMS + '"]' 27 28 class DiscoFeature(domish.Element): 29 """ 30 Element representing an XMPP service discovery feature. 31 """ 32 33 def __init__(self, feature): 34 domish.Element.__init__(self, (NS_INFO, 'feature'), 35 attribs={'var': feature}) 36 37 38 class DiscoIdentity(domish.Element): 39 """ 40 Element representing an XMPP service discovery identity. 41 """ 42 43 def __init__(self, category, type, name = None): 44 domish.Element.__init__(self, (NS_INFO, 'identity'), 45 attribs={'category': category, 46 'type': type}) 47 if name: 48 self['name'] = name 49 50 51 class DiscoItem(domish.Element): 52 """ 53 Element representing an XMPP service discovery item. 54 """ 55 56 def __init__(self, jid, node='', name=None): 57 domish.Element.__init__(self, (NS_ITEMS, 'item'), 58 attribs={'jid': jid.full()}) 59 if node: 60 self['node'] = node 61 62 if name: 63 self['name'] = name 26 DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]' 27 DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]' 28 29 class DiscoFeature(unicode): 30 """ 31 XMPP service discovery feature. 32 33 This extends C{unicode} to convert to and from L{domish.Element}, but 34 further behaves identically. 35 """ 36 37 def toElement(self): 38 """ 39 Render to a DOM representation. 40 41 @rtype: L{domish.Element}. 42 """ 43 element = domish.Element((NS_DISCO_INFO, 'feature')) 44 element['var'] = unicode(self) 45 return element 46 47 48 @staticmethod 49 def fromElement(element): 50 """ 51 Parse a DOM representation into a L{DiscoFeature} instance. 52 53 @param element: Element that represents the disco feature. 54 @type element: L{domish.Element}. 55 @rtype L{DiscoFeature}. 56 """ 57 featureURI = element.getAttribute('var', u'') 58 feature = DiscoFeature(featureURI) 59 return feature 60 61 62 63 class DiscoIdentity(object): 64 """ 65 XMPP service discovery identity. 66 67 @ivar category: The identity category. 68 @type category: C{unicode} 69 @ivar type: The identity type. 70 @type type: C{unicode} 71 @ivar name: The optional natural language name for this entity. 72 @type name: C{unicode} 73 """ 74 75 def __init__(self, category, idType, name=None): 76 self.category = category 77 self.type = idType 78 self.name = name 79 80 81 def toElement(self): 82 """ 83 Generate a DOM representation. 84 85 @rtype: L{domish.Element}. 86 """ 87 element = domish.Element((NS_DISCO_INFO, 'identity')) 88 if self.category: 89 element['category'] = self.category 90 if self.type: 91 element['type'] = self.type 92 if self.name: 93 element['name'] = self.name 94 return element 95 96 97 @staticmethod 98 def fromElement(element): 99 """ 100 Parse a DOM representation into a L{DiscoIdentity} instance. 101 102 @param element: Element that represents the disco identity. 103 @type element: L{domish.Element}. 104 @rtype L{DiscoIdentity}. 105 """ 106 category = element.getAttribute('category') 107 idType = element.getAttribute('type') 108 name = element.getAttribute('name') 109 feature = DiscoIdentity(category, idType, name) 110 return feature 111 112 113 114 class DiscoInfo(object): 115 """ 116 XMPP service discovery info. 117 118 @ivar nodeIdentifier: The optional node this info applies to. 119 @type nodeIdentifier: C{unicode} 120 @ivar features: Features as L{DiscoFeature}. 121 @type features: C{set) 122 @ivar identities: Identities as a mapping from (category, type) to name, 123 all C{unicode}. 124 @type identities: C{dict} 125 @ivar extensions: Service discovery extensions as a mapping from the 126 extension form's C{FORM_TYPE} (C{unicode}) to 127 L{data_form.Form}. Forms with no C{FORM_TYPE} field 128 are mapped as C{None}. Note that multiple forms 129 with the same C{FORM_TYPE} have the last in sequence 130 prevail. 131 @type extensions: C{dict} 132 @ivar _items: Sequence of added items. 133 @type _items: C{list} 134 """ 135 136 def __init__(self): 137 self.nodeIdentifier = '' 138 self.features = set() 139 self.identities = {} 140 self.extensions = {} 141 self._items = [] 142 143 144 def __iter__(self): 145 """ 146 Iterator over sequence of items in the order added. 147 """ 148 return iter(self._items) 149 150 151 def append(self, item): 152 """ 153 Add a piece of service discovery info. 154 155 @param item: A feature, identity or extension form. 156 @type item: L{DiscoFeature}, L{DiscoIdentity} or L{data_form.Form} 157 """ 158 self._items.append(item) 159 160 if isinstance(item, DiscoFeature): 161 self.features.add(item) 162 elif isinstance(item, DiscoIdentity): 163 self.identities[(item.category, item.type)] = item.name 164 elif isinstance(item, data_form.Form): 165 self.extensions[item.formNamespace] = item 166 167 168 def toElement(self): 169 """ 170 Generate a DOM representation. 171 172 This takes the items added with C{append} to create a DOM 173 representation of service discovery information. 174 175 @rtype: L{domish.Element}. 176 """ 177 element = domish.Element((NS_DISCO_INFO, 'query')) 178 179 if self.nodeIdentifier: 180 element['node'] = self.nodeIdentifier 181 182 for item in self: 183 element.addChild(item.toElement()) 184 185 return element 186 187 188 @staticmethod 189 def fromElement(element): 190 """ 191 Parse a DOM representation into a L{DiscoInfo} instance. 192 193 @param element: Element that represents the disco info. 194 @type element: L{domish.Element}. 195 @rtype L{DiscoInfo}. 196 """ 197 198 info = DiscoInfo() 199 200 info.nodeIdentifier = element.getAttribute('node', '') 201 202 for child in element.elements(): 203 item = None 204 205 if (child.uri, child.name) == (NS_DISCO_INFO, 'feature'): 206 item = DiscoFeature.fromElement(child) 207 elif (child.uri, child.name) == (NS_DISCO_INFO, 'identity'): 208 item = DiscoIdentity.fromElement(child) 209 elif (child.uri, child.name) == (data_form.NS_X_DATA, 'x'): 210 item = data_form.Form.fromElement(child) 211 212 if item: 213 info.append(item) 214 215 return info 216 217 218 219 class DiscoItem(object): 220 """ 221 XMPP service discovery item. 222 223 @ivar entity: The entity holding the item. 224 @type entity: L{jid.JID} 225 @ivar nodeIdentifier: The optional node identifier for the item. 226 @type nodeIdentifier: C{unicode} 227 @ivar name: The optional natural language name for this entity. 228 @type name: C{unicode} 229 """ 230 231 def __init__(self, entity, nodeIdentifier='', name=None): 232 self.entity = entity 233 self.nodeIdentifier = nodeIdentifier 234 self.name = name 235 236 237 def toElement(self): 238 """ 239 Generate a DOM representation. 240 241 @rtype: L{domish.Element}. 242 """ 243 element = domish.Element((NS_DISCO_ITEMS, 'item')) 244 if self.entity: 245 element['jid'] = self.entity.full() 246 if self.nodeIdentifier: 247 element['node'] = self.nodeIdentifier 248 if self.name: 249 element['name'] = self.name 250 return element 251 252 253 @staticmethod 254 def fromElement(element): 255 """ 256 Parse a DOM representation into a L{DiscoItem} instance. 257 258 @param element: Element that represents the disco iitem. 259 @type element: L{domish.Element}. 260 @rtype L{DiscoItem}. 261 """ 262 try: 263 entity = jid.JID(element.getAttribute('jid', ' ')) 264 except jid.InvalidFormat: 265 entity = None 266 nodeIdentifier = element.getAttribute('node', '') 267 name = element.getAttribute('name') 268 feature = DiscoItem(entity, nodeIdentifier, name) 269 return feature 270 271 272 273 class DiscoItems(object): 274 """ 275 XMPP service discovery items. 276 277 @ivar nodeIdentifier: The optional node this info applies to. 278 @type nodeIdentifier: C{unicode} 279 @ivar _items: Sequence of added items. 280 @type _items: C{list} 281 """ 282 283 def __init__(self): 284 self.nodeIdentifier = '' 285 self._items = [] 286 287 288 def __iter__(self): 289 """ 290 Iterator over sequence of items in the order added. 291 """ 292 return iter(self._items) 293 294 295 def append(self, item): 296 """ 297 Append item to the sequence of items. 298 299 @param item: Item to be added. 300 @type item: L{DiscoItem} 301 """ 302 self._items.append(item) 303 304 305 def toElement(self): 306 """ 307 Generate a DOM representation. 308 309 This takes the items added with C{append} to create a DOM 310 representation of service discovery items. 311 312 @rtype: L{domish.Element}. 313 """ 314 element = domish.Element((NS_DISCO_ITEMS, 'query')) 315 316 if self.nodeIdentifier: 317 element['node'] = self.nodeIdentifier 318 319 for item in self: 320 element.addChild(item.toElement()) 321 322 return element 323 324 325 @staticmethod 326 def fromElement(element): 327 """ 328 Parse a DOM representation into a L{DiscoItems} instance. 329 330 @param element: Element that represents the disco items. 331 @type element: L{domish.Element}. 332 @rtype L{DiscoItems}. 333 """ 334 335 info = DiscoItems() 336 337 info.nodeIdentifier = element.getAttribute('node', '') 338 339 for child in element.elements(): 340 if (child.uri, child.name) == (NS_DISCO_ITEMS, 'item'): 341 item = DiscoItem.fromElement(child) 342 info.append(item) 343 344 return info 345 346 347 348 class _DiscoRequest(xmlstream.IQ): 349 """ 350 Element representing an XMPP service discovery request. 351 """ 352 353 def __init__(self, xs, namespace, nodeIdentifier=''): 354 """ 355 Initialize the request. 356 357 @param xs: XML Stream the request should go out on. 358 @type xs: L{xmlstream.XmlStream} 359 @param namespace: Request namespace. 360 @type namespace: C{str} 361 @param nodeIdentifier: Node to request info from. 362 @type nodeIdentifier: C{unicode} 363 """ 364 xmlstream.IQ.__init__(self, xs, "get") 365 query = self.addElement((namespace, 'query')) 366 if nodeIdentifier: 367 query['node'] = nodeIdentifier 368 369 370 371 class DiscoClientProtocol(XMPPHandler): 372 """ 373 XMPP Service Discovery client protocol. 374 """ 375 376 def requestInfo(self, entity, nodeIdentifier=''): 377 """ 378 Request information discovery from a node. 379 380 @param entity: Entity to send the request to. 381 @type entity: L{jid.JID} 382 @param nodeIdentifier: Optional node to request info from. 383 @type nodeIdentifier: C{unicode} 384 """ 385 386 request = _DiscoRequest(self.xmlstream, NS_DISCO_INFO, nodeIdentifier) 387 388 d = request.send(entity.full()) 389 d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query)) 390 return d 391 392 393 def requestItems(self, entity, nodeIdentifier=''): 394 """ 395 Request items discovery from a node. 396 397 @param entity: Entity to send the request to. 398 @type entity: L{jid.JID} 399 @param nodeIdentifier: Optional node to request info from. 400 @type nodeIdentifier: C{unicode} 401 """ 402 403 request = _DiscoRequest(self.xmlstream, NS_DISCO_ITEMS, nodeIdentifier) 404 405 d = request.send(entity.full()) 406 d.addCallback(lambda iq: DiscoItems.fromElement(iq.query)) 407 return d 408 64 409 65 410 … … 96 441 self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest) 97 442 98 def _error(self, failure):99 failure.trap(defer.FirstError)100 return failure.value.subFailure101 443 102 444 def _onDiscoInfo(self, iq): 445 """ 446 Called for incoming disco info requests. 447 448 @param iq: The request iq element. 449 @type iq: L{Element<twisted.words.xish.domish.Element>} 450 """ 103 451 requestor = jid.internJID(iq["from"]) 104 452 target = jid.internJID(iq["to"]) 105 453 nodeIdentifier = iq.query.getAttribute("node", '') 106 454 107 def toResponse(results): 108 info = [] 109 for i in results: 110 info.extend(i[1]) 111 455 def toResponse(info): 112 456 if nodeIdentifier and not info: 113 457 raise error.StanzaError('item-not-found') 114 458 else: 115 response = domish.Element((NS_INFO, 'query')) 459 response = DiscoInfo() 460 response.nodeIdentifier = nodeIdentifier 116 461 117 462 for item in info: 118 response.addChild(item) 119 120 return response 121 122 dl = [] 123 for handler in self.parent: 124 if IDisco.providedBy(handler): 125 dl.append(handler.getDiscoInfo(requestor, target, 126 nodeIdentifier)) 127 128 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1) 129 d.addCallbacks(toResponse, self._error) 463 response.append(item) 464 465 return response.toElement() 466 467 d = self.info(requestor, target, nodeIdentifier) 468 d.addCallback(toResponse) 130 469 return d 131 470 471 132 472 def _onDiscoItems(self, iq): 473 """ 474 Called for incoming disco items requests. 475 476 @param iq: The request iq element. 477 @type iq: L{Element<twisted.words.xish.domish.Element>} 478 """ 133 479 requestor = jid.internJID(iq["from"]) 134 480 target = jid.internJID(iq["to"]) 135 481 nodeIdentifier = iq.query.getAttribute("node", '') 136 482 137 def toResponse(results): 138 items = [] 139 for i in results: 140 items.extend(i[1]) 141 142 response = domish.Element((NS_ITEMS, 'query')) 483 def toResponse(items): 484 response = DiscoItems() 485 response.nodeIdentifier = nodeIdentifier 143 486 144 487 for item in items: 145 response.addChild(item) 146 147 return response 148 149 dl = [] 150 for handler in self.parent: 151 if IDisco.providedBy(handler): 152 dl.append(handler.getDiscoItems(requestor, target, 153 nodeIdentifier)) 154 155 d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1) 156 d.addCallbacks(toResponse, self._error) 488 response.append(item) 489 490 return response.toElement() 491 492 d = self.items(requestor, target, nodeIdentifier) 493 d.addCallback(toResponse) 157 494 return d 495 496 497 def _gatherResults(self, deferredList): 498 """ 499 Gather results from a list of deferreds. 500 501 Similar to L{defer.gatherResults}, but flattens the returned results, 502 consumes errors after the first one and fires the errback of the 503 returned deferred with the failure of the first deferred that fires its 504 errback. 505 506 @param deferredList: List of deferreds for which the results should be 507 gathered. 508 @type deferredList: C{list} 509 @return: Deferred that fires with a list of gathered results. 510 @rtype: L{defer.Deferred} 511 """ 512 def cb(resultList): 513 results = [] 514 for success, value in resultList: 515 results.extend(value) 516 return results 517 518 def eb(failure): 519 failure.trap(defer.FirstError) 520 return failure.value.subFailure 521 522 d = defer.DeferredList(deferredList, fireOnOneErrback=1, 523 consumeErrors=1) 524 d.addCallbacks(cb, eb) 525 return d 526 527 528 def info(self, requestor, target, nodeIdentifier): 529 """ 530 Inspect all sibling protocol handlers for disco info. 531 532 Calls the L{getDiscoInfo<IDisco.getDiscoInfo>} method on all child 533 handlers of the parent, that provide L{IDisco}. 534 535 @param requestor: The entity that sent the request. 536 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>} 537 @param target: The entity the request was sent to. 538 @type target: L{JID<twisted.words.protocols.jabber.jid.JID>} 539 @param nodeIdentifier: The optional node being queried, or C{''}. 540 @type nodeIdentifier: C{unicode} 541 @return: Deferred with the gathered results from sibling handlers. 542 @rtype: L{defer.Deferred} 543 """ 544 dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier) 545 for handler in self.parent 546 if IDisco.providedBy(handler)] 547 return self._gatherResults(dl) 548 549 550 def items(self, requestor, target, nodeIdentifier): 551 """ 552 Inspect all sibling protocol handlers for disco items. 553 554 Calls the L{getDiscoItems<IDisco.getDiscoItems>} method on all child 555 handlers of the parent, that provide L{IDisco}. 556 557 @param requestor: The entity that sent the request. 558 @type requestor: L{JID<twisted.words.protocols.jabber.jid.JID>} 559 @param target: The entity the request was sent to. 560 @type target: L{JID<twisted.words.protocols.jabber.jid.JID>} 561 @param nodeIdentifier: The optional node being queried, or C{''}. 562 @type nodeIdentifier: C{unicode} 563 @return: Deferred with the gathered results from sibling handlers. 564 @rtype: L{defer.Deferred} 565 """ 566 dl = [handler.getDiscoItems(requestor, target, nodeIdentifier) 567 for handler in self.parent 568 if IDisco.providedBy(handler)] 569 return self._gatherResults(dl) -
wokkel/generic.py
r30 r57 10 10 from zope.interface import implements 11 11 12 from twisted.internet import defer 13 from twisted.words.protocols.jabber import error 12 from twisted.internet import defer, protocol 13 from twisted.words.protocols.jabber import error, jid, xmlstream 14 14 from twisted.words.protocols.jabber.xmlstream import toResponse 15 from twisted.words.xish import domish 15 from twisted.words.xish import domish, utility 16 17 try: 18 from twisted.words.xish.xmlstream import BootstrapMixin 19 except ImportError: 20 from wokkel.compat import BootstrapMixin 16 21 17 22 from wokkel import disco … … 121 126 def getDiscoItems(self, requestor, target, node): 122 127 return defer.succeed([]) 128 129 130 131 class XmlPipe(object): 132 """ 133 XML stream pipe. 134 135 Connects two objects that communicate stanzas through an XML stream like 136 interface. Each of the ends of the pipe (sink and source) can be used to 137 send XML stanzas to the other side, or add observers to process XML stanzas 138 that were sent from the other side. 139 140 XML pipes are usually used in place of regular XML streams that are 141 transported over TCP. This is the reason for the use of the names source 142 and sink for both ends of the pipe. The source side corresponds with the 143 entity that initiated the TCP connection, whereas the sink corresponds with 144 the entity that accepts that connection. In this object, though, the source 145 and sink are treated equally. 146 147 Unlike Jabber 148 L{XmlStream<twisted.words.protocols.jabber.xmlstream.XmlStream>}s, the sink 149 and source objects are assumed to represent an eternal connected and 150 initialized XML stream. As such, events corresponding to connection, 151 disconnection, initialization and stream errors are not dispatched or 152 processed. 153 154 @ivar source: Source XML stream. 155 @ivar sink: Sink XML stream. 156 """ 157 158 def __init__(self): 159 self.source = utility.EventDispatcher() 160 self.sink = utility.EventDispatcher() 161 self.source.send = lambda obj: self.sink.dispatch(obj) 162 self.sink.send = lambda obj: self.source.dispatch(obj) 163 164 165 166 class Stanza(object): 167 """ 168 Abstract representation of a stanza. 169 170 @ivar sender: The sending entity. 171 @type sender: L{jid.JID} 172 @ivar recipient: The receiving entity. 173 @type recipient: L{jid.JID} 174 """ 175 176 sender = None 177 recipient = None 178 stanzaType = None 179 180 @classmethod 181 def fromElement(Class, element): 182 stanza = Class() 183 stanza.parseElement(element) 184 return stanza 185 186 187 def parseElement(self, element): 188 self.sender = jid.internJID(element['from']) 189 if element.hasAttribute('from'): 190 self.sender = jid.internJID(element['from']) 191 if element.hasAttribute('to'): 192 self.recipient = jid.internJID(element['to']) 193 self.stanzaType = element.getAttribute('type') 194 195 196 class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory): 197 protocol = xmlstream.XmlStream 198 199 def __init__(self, authenticator): 200 BootstrapMixin.__init__(self) 201 202 self.authenticator = authenticator 203 204 deferred = defer.Deferred() 205 self.deferred = deferred 206 self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback) 207 self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback) 208 209 210 def buildProtocol(self, addr): 211 """ 212 Create an instance of XmlStream. 213 214 A new authenticator instance will be created and passed to the new 215 XmlStream. Registered bootstrap event observers are installed as well. 216 """ 217 xs = self.protocol(self.authenticator) 218 xs.factory = self 219 self.installBootstraps(xs) 220 return xs 221 222 223 def clientConnectionFailed(self, connector, reason): 224 self.deferred.errback(reason) -
wokkel/iwokkel.py
r113 r128 16 16 """ 17 17 18 manager = Attribute("""XML stream manager""")18 parent = Attribute("""XML stream manager for this handler""") 19 19 xmlstream = Attribute("""The managed XML stream""") 20 20 … … 26 26 """ 27 27 28 28 29 def disownHandlerParent(parent): 29 30 """ … … 32 33 @type parent: L{IXMPPHandlerCollection} 33 34 """ 35 34 36 35 37 def makeConnection(xs): … … 45 47 @type xs: L{XmlStream<twisted.words.protocols.jabber.XmlStream>} 46 48 """ 49 47 50 48 51 def connectionMade(): … … 55 58 """ 56 59 60 57 61 def connectionInitialized(): 58 62 """ … … 64 68 """ 65 69 70 66 71 def connectionLost(reason): 67 72 """ … … 73 78 @type reason: L{twisted.python.failure.Failure} 74 79 """ 80 75 81 76 82 … … 87 93 """ 88 94 95 89 96 def addHandler(handler): 90 97 """ … … 94 101 """ 95 102 103 96 104 def removeHandler(handler): 97 105 """ … … 100 108 @type handler: L{IXMPPHandler} 101 109 """ 110 102 111 103 112 … … 270 279 """ 271 280 272 def notifyDelete(service, nodeIdentifier, subscriptions): 281 282 def notifyDelete(service, nodeIdentifier, subscribers, 283 redirectURI=None): 273 284 """ 274 285 Send out node deletion notifications. … … 278 289 @param nodeIdentifier: The identifier of the node that was deleted. 279 290 @type nodeIdentifier: C{unicode} 280 @param subscriptions: The subscriptions for which a notification should 281 be sent out. 282 @type subscriptions: C{list} of L{jid.JID} 291 @param subscribers: The subscribers for which a notification should 292 be sent out. 293 @type subscribers: C{list} of L{jid.JID} 294 @param redirectURI: Optional XMPP URI of another node that subscribers 295 are redirected to. 296 @type redirectURI: C{str} 283 297 """ 284 298 … … 387 401 that option in a dictionary: 388 402 389 - C{'type'} (C{str}): The option's type (see390 L{Field<wokkel.data_form.Field>}'s doc string for possible values).391 - C{'label'} (C{unicode}): A human readable label for this option.392 - C{'options'} (C{dict}): Optional list of possible values for this393 option.403 - C{'type'} (C{str}): The option's type (see 404 L{Field<wokkel.data_form.Field>}'s doc string for possible values). 405 - C{'label'} (C{unicode}): A human readable label for this option. 406 - C{'options'} (C{dict}): Optional list of possible values for this 407 option. 394 408 395 409 Example:: … … 414 428 """ 415 429 416 def getDefaultConfiguration(requestor, service ):430 def getDefaultConfiguration(requestor, service, nodeType): 417 431 """ 418 432 Called when a default node configuration request has been received. … … 422 436 @param service: The entity the request was addressed to. 423 437 @type service: L{jid.JID} 438 @param nodeType: The type of node for which the configuration is 439 retrieved, C{'leaf'} or C{'collection'}. 440 @type nodeType: C{str} 424 441 @return: A deferred that fires with a C{dict} representing the default 425 442 node configuration. Keys are C{str}s that represent the … … 511 528 @type nodeIdentifier: C{unicode} 512 529 """ 530 531 532 533 class IPubSubResource(Interface): 534 535 def locateResource(request): 536 """ 537 Locate a resource that will handle the request. 538 539 @param request: The publish-subscribe request. 540 @type request: L{wokkel.pubsub.PubSubRequest} 541 """ 542 543 544 def getInfo(requestor, service, nodeIdentifier): 545 """ 546 Get node type and meta data. 547 548 @param requestor: The entity the request originated from. 549 @type requestor: L{jid.JID} 550 @param service: The publish-subscribe service entity. 551 @type service: L{jid.JID} 552 @param nodeIdentifier: Identifier of the node to request the info for. 553 @type nodeIdentifier: L{unicode} 554 @return: A deferred that fires with a dictionary. If not empty, 555 it must have the keys C{'type'} and C{'meta-data'} to keep 556 respectively the node type and a dictionary with the meta 557 data for that node. 558 @rtype: L{defer.Deferred} 559 """ 560 561 562 def getNodes(requestor, service, nodeIdentifier): 563 """ 564 Get all nodes contained by this node. 565 566 @param requestor: The entity the request originated from. 567 @type requestor: L{jid.JID} 568 @param service: The publish-subscribe service entity. 569 @type service: L{jid.JID} 570 @param nodeIdentifier: Identifier of the node to request the childs for. 571 @type nodeIdentifier: L{unicode} 572 @return: A deferred that fires with a list of child node identifiers. 573 @rtype: L{defer.Deferred} 574 """ 575 576 577 def getConfigurationOptions(): 578 """ 579 Retrieve all known node configuration options. 580 581 The returned dictionary holds the possible node configuration options 582 by option name. The value of each entry represents the specifics for 583 that option in a dictionary: 584 585 - C{'type'} (C{str}): The option's type (see 586 L{Field<wokkel.data_form.Field>}'s doc string for possible values). 587 - C{'label'} (C{unicode}): A human readable label for this option. 588 - C{'options'} (C{dict}): Optional list of possible values for this 589 option. 590 591 Example:: 592 593 { 594 "pubsub#persist_items": 595 {"type": "boolean", 596 "label": "Persist items to storage"}, 597 "pubsub#deliver_payloads": 598 {"type": "boolean", 599 "label": "Deliver payloads with event notifications"}, 600 "pubsub#send_last_published_item": 601 {"type": "list-single", 602 "label": "When to send the last published item", 603 "options": { 604 "never": "Never", 605 "on_sub": "When a new subscription is processed"} 606 } 607 } 608 609 @rtype: C{dict}. 610 """ 611 612 613 def publish(request): 614 """ 615 Called when a publish request has been received. 616 617 @param request: The publish-subscribe request. 618 @type request: L{wokkel.pubsub.PubSubRequest} 619 @return: deferred that fires on success. 620 @rtype: L{defer.Deferred} 621 """ 622 623 624 def subscribe(request): 625 """ 626 Called when a subscribe request has been received. 627 628 @param request: The publish-subscribe request. 629 @type request: L{wokkel.pubsub.PubSubRequest} 630 @return: A deferred that fires with a 631 L{Subscription<wokkel.pubsub.Subscription>}. 632 @rtype: L{defer.Deferred} 633 """ 634 635 636 def unsubscribe(request): 637 """ 638 Called when a subscribe request has been received. 639 640 @param request: The publish-subscribe request. 641 @type request: L{wokkel.pubsub.PubSubRequest} 642 @return: A deferred that fires with C{None} when unsubscription has 643 succeeded. 644 @rtype: L{defer.Deferred} 645 """ 646 647 648 def subscriptions(request): 649 """ 650 Called when a subscriptions retrieval request has been received. 651 652 @param request: The publish-subscribe request. 653 @type request: L{wokkel.pubsub.PubSubRequest} 654 @return: A deferred that fires with a C{list} of subscriptions as 655 L{Subscription<wokkel.pubsub.Subscription>}. 656 @rtype: L{defer.Deferred} 657 """ 658 659 660 def affiliations(request): 661 """ 662 Called when a affiliations retrieval request has been received. 663 664 @param request: The publish-subscribe request. 665 @type request: L{wokkel.pubsub.PubSubRequest} 666 @return: A deferred that fires with a C{list} of affiliations as 667 C{tuple}s of (node identifier as C{unicode}, affiliation state 668 as C{str}). The affiliation can be C{'owner'}, C{'publisher'}, 669 or C{'outcast'}. 670 @rtype: L{defer.Deferred} 671 """ 672 673 674 def create(request): 675 """ 676 Called when a node creation request has been received. 677 678 @param request: The publish-subscribe request. 679 @type request: L{wokkel.pubsub.PubSubRequest} 680 @return: A deferred that fires with a C{unicode} that represents 681 the identifier of the new node. 682 @rtype: L{defer.Deferred} 683 """ 684 685 686 def default(request): 687 """ 688 Called when a default node configuration request has been received. 689 690 @param request: The publish-subscribe request. 691 @type request: L{wokkel.pubsub.PubSubRequest} 692 @return: A deferred that fires with a C{dict} representing the default 693 node configuration. Keys are C{str}s that represent the 694 field name. Values can be of types C{unicode}, C{int} or 695 C{bool}. 696 @rtype: L{defer.Deferred} 697 """ 698 699 700 def configureGet(request): 701 """ 702 Called when a node configuration retrieval request has been received. 703 704 @param request: The publish-subscribe request. 705 @type request: L{wokkel.pubsub.PubSubRequest} 706 @return: A deferred that fires with a C{dict} representing the node 707 configuration. Keys are C{str}s that represent the field name. 708 Values can be of types C{unicode}, C{int} or C{bool}. 709 @rtype: L{defer.Deferred} 710 """ 711 712 713 def configureSet(request): 714 """ 715 Called when a node configuration change request has been received. 716 717 @param request: The publish-subscribe request. 718 @type request: L{wokkel.pubsub.PubSubRequest} 719 @return: A deferred that fires with C{None} when the node's 720 configuration has been changed. 721 @rtype: L{defer.Deferred} 722 """ 723 724 725 def items(request): 726 """ 727 Called when a items retrieval request has been received. 728 729 @param request: The publish-subscribe request. 730 @type request: L{wokkel.pubsub.PubSubRequest} 731 @return: A deferred that fires with a C{list} of L{pubsub.Item}. 732 @rtype: L{defer.Deferred} 733 """ 734 735 736 def retract(request): 737 """ 738 Called when a item retraction request has been received. 739 740 @param request: The publish-subscribe request. 741 @type request: L{wokkel.pubsub.PubSubRequest} 742 @return: A deferred that fires with C{None} when the given items have 743 been retracted. 744 @rtype: L{defer.Deferred} 745 """ 746 747 748 def purge(request): 749 """ 750 Called when a node purge request has been received. 751 752 @param request: The publish-subscribe request. 753 @type request: L{wokkel.pubsub.PubSubRequest} 754 @return: A deferred that fires with C{None} when the node has been 755 purged. 756 @rtype: L{defer.Deferred} 757 """ 758 759 760 def delete(request): 761 """ 762 Called when a node deletion request has been received. 763 764 @param request: The publish-subscribe request. 765 @type request: L{wokkel.pubsub.PubSubRequest} 766 @return: A deferred that fires with C{None} when the node has been 767 deleted. 768 @rtype: L{defer.Deferred} 769 """ 770 513 771 514 772 … … 655 913 """ 656 914 """ 657 -
wokkel/pubsub.py
r30 r59 14 14 15 15 from twisted.internet import defer 16 from twisted.python import log 16 17 from twisted.words.protocols.jabber import jid, error, xmlstream 17 18 from twisted.words.xish import domish 18 19 19 from wokkel import disco, data_form, shim20 from wokkel import disco, data_form, generic, shim 20 21 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 21 from wokkel.iwokkel import IPubSubClient, IPubSubService 22 from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource 22 23 23 24 # Iq get and set XPath queries … … 32 33 NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config" 33 34 NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data" 34 35 # In publish-subscribe namespace XPath query selector. 36 IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]' 37 IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]' 38 39 # Publish-subscribe XPath queries 40 PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB 41 PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER 42 PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT 43 PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT 44 PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT 45 PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT 46 47 # Publish-subscribe command XPath queries 48 PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB 49 PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB 50 PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB 51 PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB 52 PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB 53 PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB 54 PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER 55 PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER 56 PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER 57 PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB 58 PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB 59 PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \ 60 IN_NS_PUBSUB_OWNER 61 PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \ 62 IN_NS_PUBSUB_OWNER 63 PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \ 64 IN_NS_PUBSUB_OWNER 65 PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \ 66 IN_NS_PUBSUB_OWNER 67 PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB 68 PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB 69 PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER 70 PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER 35 NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options" 36 37 # XPath to match pubsub requests 38 PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \ 39 'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \ 40 '@xmlns="' + NS_PUBSUB_OWNER + '"]' 71 41 72 42 class SubscriptionPending(Exception): … … 99 69 100 70 101 class BadRequest( PubSubError):71 class BadRequest(error.StanzaError): 102 72 """ 103 73 Bad request stanza error. 104 74 """ 105 75 def __init__(self, pubsubCondition=None, text=None): 106 PubSubError.__init__(self, 'bad-request', pubsubCondition, text) 76 if pubsubCondition: 77 appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) 78 else: 79 appCondition = None 80 error.StanzaError.__init__(self, 'bad-request', 81 text=text, 82 appCondition=appCondition) 107 83 108 84 … … 110 86 class Unsupported(PubSubError): 111 87 def __init__(self, feature, text=None): 88 self.feature = feature 112 89 PubSubError.__init__(self, 'feature-not-implemented', 113 90 'unsupported', … … 115 92 text) 116 93 94 def __str__(self): 95 message = PubSubError.__str__(self) 96 message += ', feature %r' % self.feature 97 return message 117 98 118 99 … … 168 149 169 150 170 class _PubSubRequest(xmlstream.IQ): 171 """ 172 Publish subscribe request. 173 174 @ivar verb: Request verb 175 @type verb: C{str} 176 @ivar namespace: Request namespace. 177 @type namespace: C{str} 178 @ivar method: Type attribute of the IQ request. Either C{'set'} or C{'get'} 179 @type method: C{str} 180 @ivar command: Command element of the request. This is the direct child of 181 the C{pubsub} element in the C{namespace} with the name 182 C{verb}. 183 """ 184 185 def __init__(self, xs, verb, namespace=NS_PUBSUB, method='set'): 186 xmlstream.IQ.__init__(self, xs, method) 187 self.addElement((namespace, 'pubsub')) 188 189 self.command = self.pubsub.addElement(verb) 190 191 192 def send(self, to): 193 """ 194 Send out request. 195 196 Extends L{xmlstream.IQ.send} by requiring the C{to} parameter to be 197 a L{JID} instance. 198 199 @param to: Entity to send the request to. 200 @type to: L{JID} 201 """ 202 destination = to.full() 203 return xmlstream.IQ.send(self, destination) 151 class PubSubRequest(generic.Stanza): 152 """ 153 A publish-subscribe request. 154 155 The set of instance variables used depends on the type of request. If 156 a variable is not applicable or not passed in the request, its value is 157 C{None}. 158 159 @ivar verb: The type of publish-subscribe request. See L{_requestVerbMap}. 160 @type verb: C{str}. 161 162 @ivar affiliations: Affiliations to be modified. 163 @type affiliations: C{set} 164 @ivar items: The items to be published, as L{domish.Element}s. 165 @type items: C{list} 166 @ivar itemIdentifiers: Identifiers of the items to be retrieved or 167 retracted. 168 @type itemIdentifiers: C{set} 169 @ivar maxItems: Maximum number of items to retrieve. 170 @type maxItems: C{int}. 171 @ivar nodeIdentifier: Identifier of the node the request is about. 172 @type nodeIdentifier: C{unicode} 173 @ivar nodeType: The type of node that should be created, or for which the 174 configuration is retrieved. C{'leaf'} or C{'collection'}. 175 @type nodeType: C{str} 176 @ivar options: Configurations options for nodes, subscriptions and publish 177 requests. 178 @type options: L{data_form.Form} 179 @ivar subscriber: The subscribing entity. 180 @type subscriber: L{JID} 181 @ivar subscriptionIdentifier: Identifier for a specific subscription. 182 @type subscriptionIdentifier: C{unicode} 183 @ivar subscriptions: Subscriptions to be modified, as a set of 184 L{Subscription}. 185 @type subscriptions: C{set} 186 """ 187 188 verb = None 189 190 affiliations = None 191 items = None 192 itemIdentifiers = None 193 maxItems = None 194 nodeIdentifier = None 195 nodeType = None 196 options = None 197 subscriber = None 198 subscriptionIdentifier = None 199 subscriptions = None 200 201 # Map request iq type and subelement name to request verb 202 _requestVerbMap = { 203 ('set', NS_PUBSUB, 'publish'): 'publish', 204 ('set', NS_PUBSUB, 'subscribe'): 'subscribe', 205 ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe', 206 ('get', NS_PUBSUB, 'options'): 'optionsGet', 207 ('set', NS_PUBSUB, 'options'): 'optionsSet', 208 ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions', 209 ('get', NS_PUBSUB, 'affiliations'): 'affiliations', 210 ('set', NS_PUBSUB, 'create'): 'create', 211 ('get', NS_PUBSUB_OWNER, 'default'): 'default', 212 ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet', 213 ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet', 214 ('get', NS_PUBSUB, 'items'): 'items', 215 ('set', NS_PUBSUB, 'retract'): 'retract', 216 ('set', NS_PUBSUB_OWNER, 'purge'): 'purge', 217 ('set', NS_PUBSUB_OWNER, 'delete'): 'delete', 218 ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet', 219 ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet', 220 ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet', 221 ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet', 222 } 223 224 # Map request verb to request iq type and subelement name 225 _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems())) 226 227 # Map request verb to parameter handler names 228 _parameters = { 229 'publish': ['node', 'items'], 230 'subscribe': ['nodeOrEmpty', 'jid'], 231 'unsubscribe': ['nodeOrEmpty', 'jid'], 232 'optionsGet': ['nodeOrEmpty', 'jid'], 233 'optionsSet': ['nodeOrEmpty', 'jid', 'options'], 234 'subscriptions': [], 235 'affiliations': [], 236 'create': ['nodeOrNone'], 237 'default': ['default'], 238 'configureGet': ['nodeOrEmpty'], 239 'configureSet': ['nodeOrEmpty', 'configure'], 240 'items': ['node', 'maxItems', 'itemIdentifiers'], 241 'retract': ['node', 'itemIdentifiers'], 242 'purge': ['node'], 243 'delete': ['node'], 244 'affiliationsGet': ['nodeOrEmpty'], 245 'affiliationsSet': [], 246 'subscriptionsGet': ['nodeOrEmpty'], 247 'subscriptionsSet': [], 248 } 249 250 def __init__(self, verb=None): 251 self.verb = verb 252 253 254 @staticmethod 255 def _findForm(element, formNamespace): 256 """ 257 Find a Data Form. 258 259 Look for an element that represents a Data Form with the specified 260 form namespace as a child element of the given element. 261 """ 262 if not element: 263 return None 264 265 form = None 266 for child in element.elements(): 267 try: 268 form = data_form.Form.fromElement(child) 269 except data_form.Error: 270 continue 271 272 if form.formNamespace != NS_PUBSUB_NODE_CONFIG: 273 continue 274 275 return form 276 277 278 def _parse_node(self, verbElement): 279 """ 280 Parse the required node identifier out of the verbElement. 281 """ 282 try: 283 self.nodeIdentifier = verbElement["node"] 284 except KeyError: 285 raise BadRequest('nodeid-required') 286 287 288 def _render_node(self, verbElement): 289 """ 290 Render the required node identifier on the verbElement. 291 """ 292 if not self.nodeIdentifier: 293 raise Exception("Node identifier is required") 294 295 verbElement['node'] = self.nodeIdentifier 296 297 298 def _parse_nodeOrEmpty(self, verbElement): 299 """ 300 Parse the node identifier out of the verbElement. May be empty. 301 """ 302 self.nodeIdentifier = verbElement.getAttribute("node", '') 303 304 305 def _render_nodeOrEmpty(self, verbElement): 306 """ 307 Render the node identifier on the verbElement. May be empty. 308 """ 309 if self.nodeIdentifier: 310 verbElement['node'] = self.nodeIdentifier 311 312 313 def _parse_nodeOrNone(self, verbElement): 314 """ 315 Parse the optional node identifier out of the verbElement. 316 """ 317 self.nodeIdentifier = verbElement.getAttribute("node") 318 319 320 def _render_nodeOrNone(self, verbElement): 321 """ 322 Render the optional node identifier on the verbElement. 323 """ 324 if self.nodeIdentifier: 325 verbElement['node'] = self.nodeIdentifier 326 327 328 def _parse_items(self, verbElement): 329 """ 330 Parse items out of the verbElement for publish requests. 331 """ 332 self.items = [] 333 for element in verbElement.elements(): 334 if element.uri == NS_PUBSUB and element.name == 'item': 335 self.items.append(element) 336 337 338 def _render_items(self, verbElement): 339 """ 340 Render items into the verbElement for publish requests. 341 """ 342 if self.items: 343 for item in self.items: 344 verbElement.addChild(item) 345 346 347 def _parse_jid(self, verbElement): 348 """ 349 Parse subscriber out of the verbElement for un-/subscribe requests. 350 """ 351 try: 352 self.subscriber = jid.internJID(verbElement["jid"]) 353 except KeyError: 354 raise BadRequest('jid-required') 355 356 357 def _render_jid(self, verbElement): 358 """ 359 Render subscriber into the verbElement for un-/subscribe requests. 360 """ 361 verbElement['jid'] = self.subscriber.full() 362 363 364 def _parse_default(self, verbElement): 365 """ 366 Parse node type out of a request for the default node configuration. 367 """ 368 form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG) 369 if form and form.formType == 'submit': 370 values = form.getValues() 371 self.nodeType = values.get('pubsub#node_type', 'leaf') 372 else: 373 self.nodeType = 'leaf' 374 375 376 def _parse_configure(self, verbElement): 377 """ 378 Parse options out of a request for setting the node configuration. 379 """ 380 form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG) 381 if form: 382 if form.formType == 'submit': 383 self.options = form.getValues() 384 elif form.formType == 'cancel': 385 self.options = {} 386 else: 387 raise BadRequest(text="Unexpected form type %r" % form.formType) 388 else: 389 raise BadRequest(text="Missing configuration form") 390 391 392 393 def _parse_itemIdentifiers(self, verbElement): 394 """ 395 Parse item identifiers out of items and retract requests. 396 """ 397 self.itemIdentifiers = [] 398 for element in verbElement.elements(): 399 if element.uri == NS_PUBSUB and element.name == 'item': 400 try: 401 self.itemIdentifiers.append(element["id"]) 402 except KeyError: 403 raise BadRequest() 404 405 406 def _render_itemIdentifiers(self, verbElement): 407 """ 408 Render item identifiers into items and retract requests. 409 """ 410 if self.itemIdentifiers: 411 for itemIdentifier in self.itemIdentifiers: 412 item = verbElement.addElement('item') 413 item['id'] = itemIdentifier 414 415 416 def _parse_maxItems(self, verbElement): 417 """ 418 Parse maximum items out of an items request. 419 """ 420 value = verbElement.getAttribute('max_items') 421 422 if value: 423 try: 424 self.maxItems = int(value) 425 except ValueError: 426 raise BadRequest(text="Field max_items requires a positive " + 427 "integer value") 428 429 430 def _render_maxItems(self, verbElement): 431 """ 432 Parse maximum items into an items request. 433 """ 434 if self.maxItems: 435 verbElement['max_items'] = unicode(self.maxItems) 436 437 438 def _parse_options(self, verbElement): 439 form = PubSubRequest._findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS) 440 if form: 441 if form.formType == 'submit': 442 self.options = form.getValues() 443 elif form.formType == 'cancel': 444 self.options = {} 445 else: 446 raise BadRequest(text="Unexpected form type %r" % form.formType) 447 else: 448 raise BadRequest(text="Missing options form") 449 450 def parseElement(self, element): 451 """ 452 Parse the publish-subscribe verb and parameters out of a request. 453 """ 454 generic.Stanza.parseElement(self, element) 455 456 for child in element.pubsub.elements(): 457 key = (self.stanzaType, child.uri, child.name) 458 try: 459 verb = self._requestVerbMap[key] 460 except KeyError: 461 continue 462 else: 463 self.verb = verb 464 break 465 466 if not self.verb: 467 raise NotImplementedError() 468 469 for parameter in self._parameters[verb]: 470 getattr(self, '_parse_%s' % parameter)(child) 471 472 473 def send(self, xs): 474 """ 475 Send this request to its recipient. 476 477 This renders all of the relevant parameters for this specific 478 requests into an L{xmlstream.IQ}, and invoke its C{send} method. 479 This returns a deferred that fires upon reception of a response. See 480 L{xmlstream.IQ} for details. 481 482 @param xs: The XML stream to send the request on. 483 @type xs: L{xmlstream.XmlStream} 484 @rtype: L{defer.Deferred}. 485 """ 486 487 try: 488 (self.stanzaType, 489 childURI, 490 childName) = self._verbRequestMap[self.verb] 491 except KeyError: 492 raise NotImplementedError() 493 494 iq = xmlstream.IQ(xs, self.stanzaType) 495 iq.addElement((childURI, 'pubsub')) 496 verbElement = iq.pubsub.addElement(childName) 497 498 if self.sender: 499 iq['from'] = self.sender.full() 500 if self.recipient: 501 iq['to'] = self.recipient.full() 502 503 for parameter in self._parameters[self.verb]: 504 getattr(self, '_render_%s' % parameter)(verbElement) 505 506 return iq.send() 204 507 205 508 … … 246 549 """ 247 550 551 redirectURI = None 552 248 553 249 554 … … 303 608 nodeIdentifier = action["node"] 304 609 event = DeleteEvent(sender, recipient, nodeIdentifier, headers) 610 if action.redirect: 611 event.redirectURI = action.redirect.getAttribute('uri') 305 612 self.deleteReceived(event) 306 613 … … 324 631 325 632 326 def createNode(self, service, nodeIdentifier=None ):633 def createNode(self, service, nodeIdentifier=None, sender=None): 327 634 """ 328 635 Create a publish subscribe node. … … 333 640 @type nodeIdentifier: C{unicode} 334 641 """ 335 336 337 request = _PubSubRequest(self.xmlstream, 'create') 338 if nodeIdentifier: 339 request.command['node'] = nodeIdentifier 642 request = PubSubRequest('create') 643 request.recipient = service 644 request.nodeIdentifier = nodeIdentifier 645 request.sender = sender 340 646 341 647 def cb(iq): … … 347 653 return new_node 348 654 349 return request.send(service).addCallback(cb) 350 351 352 def deleteNode(self, service, nodeIdentifier): 655 d = request.send(self.xmlstream) 656 d.addCallback(cb) 657 return d 658 659 660 def deleteNode(self, service, nodeIdentifier, sender=None): 353 661 """ 354 662 Delete a publish subscribe node. … … 359 667 @type nodeIdentifier: C{unicode} 360 668 """ 361 request = _PubSubRequest(self.xmlstream, 'delete') 362 request.command['node'] = nodeIdentifier 363 return request.send(service) 364 365 366 def subscribe(self, service, nodeIdentifier, subscriber): 669 request = PubSubRequest('delete') 670 request.recipient = service 671 request.nodeIdentifier = nodeIdentifier 672 request.sender = sender 673 return request.send(self.xmlstream) 674 675 676 def subscribe(self, service, nodeIdentifier, subscriber, sender=None): 367 677 """ 368 678 Subscribe to a publish subscribe node. … … 376 686 @type subscriber: L{JID} 377 687 """ 378 request = _PubSubRequest(self.xmlstream, 'subscribe') 379 if nodeIdentifier: 380 request.command['node'] = nodeIdentifier 381 request.command['jid'] = subscriber.full() 688 request = PubSubRequest('subscribe') 689 request.recipient = service 690 request.nodeIdentifier = nodeIdentifier 691 request.subscriber = subscriber 692 request.sender = sender 382 693 383 694 def cb(iq): … … 394 705 return None 395 706 396 return request.send(service).addCallback(cb) 397 398 399 def unsubscribe(self, service, nodeIdentifier, subscriber): 707 d = request.send(self.xmlstream) 708 d.addCallback(cb) 709 return d 710 711 712 def unsubscribe(self, service, nodeIdentifier, subscriber, sender=None): 400 713 """ 401 714 Unsubscribe from a publish subscribe node. … … 408 721 @type subscriber: L{JID} 409 722 """ 410 request = _PubSubRequest(self.xmlstream, 'unsubscribe') 411 if nodeIdentifier: 412 request.command['node'] = nodeIdentifier 413 request.command['jid'] = subscriber.full() 414 return request.send(service) 415 416 417 def publish(self, service, nodeIdentifier, items=None): 723 request = PubSubRequest('unsubscribe') 724 request.recipient = service 725 request.nodeIdentifier = nodeIdentifier 726 request.subscriber = subscriber 727 request.sender = sender 728 return request.send(self.xmlstream) 729 730 731 def publish(self, service, nodeIdentifier, items=None, sender=None): 418 732 """ 419 733 Publish to a publish subscribe node. … … 426 740 @type items: C{list} 427 741 """ 428 request = _PubSubRequest(self.xmlstream, 'publish') 429 request.command['node'] = nodeIdentifier 430 if items: 431 for item in items: 432 request.command.addChild(item) 433 434 return request.send(service) 435 436 437 def items(self, service, nodeIdentifier, maxItems=None): 742 request = PubSubRequest('publish') 743 request.recipient = service 744 request.nodeIdentifier = nodeIdentifier 745 request.items = items 746 request.sender = sender 747 return request.send(self.xmlstream) 748 749 750 def items(self, service, nodeIdentifier, maxItems=None, sender=None): 438 751 """ 439 752 Retrieve previously published items from a publish subscribe node. … … 446 759 @type maxItems: C{int} 447 760 """ 448 request = _PubSubRequest(self.xmlstream, 'items', method='get')449 if nodeIdentifier:450 request.command['node']= nodeIdentifier761 request = PubSubRequest('items') 762 request.recipient = service 763 request.nodeIdentifier = nodeIdentifier 451 764 if maxItems: 452 request.command["max_items"] = str(int(maxItems)) 765 request.maxItems = str(int(maxItems)) 766 request.sender = sender 453 767 454 768 def cb(iq): … … 459 773 return items 460 774 461 return request.send(service).addCallback(cb) 775 d = request.send(self.xmlstream) 776 d.addCallback(cb) 777 return d 462 778 463 779 … … 494 810 495 811 iqHandlers = { 496 PUBSUB_PUBLISH: '_onPublish', 497 PUBSUB_CREATE: '_onCreate', 498 PUBSUB_SUBSCRIBE: '_onSubscribe', 499 PUBSUB_OPTIONS_GET: '_onOptionsGet', 500 PUBSUB_OPTIONS_SET: '_onOptionsSet', 501 PUBSUB_AFFILIATIONS: '_onAffiliations', 502 PUBSUB_ITEMS: '_onItems', 503 PUBSUB_RETRACT: '_onRetract', 504 PUBSUB_SUBSCRIPTIONS: '_onSubscriptions', 505 PUBSUB_UNSUBSCRIBE: '_onUnsubscribe', 506 507 PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet', 508 PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet', 509 PUBSUB_CONFIGURE_GET: '_onConfigureGet', 510 PUBSUB_CONFIGURE_SET: '_onConfigureSet', 511 PUBSUB_DEFAULT: '_onDefault', 512 PUBSUB_PURGE: '_onPurge', 513 PUBSUB_DELETE: '_onDelete', 514 PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet', 515 PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet', 516 812 '/*': '_onPubSubRequest', 517 813 } 518 814 519 520 def __init__(self): 815 _legacyHandlers = { 816 'publish': ('publish', ['sender', 'recipient', 817 'nodeIdentifier', 'items']), 818 'subscribe': ('subscribe', ['sender', 'recipient', 819 'nodeIdentifier', 'subscriber']), 820 'unsubscribe': ('unsubscribe', ['sender', 'recipient', 821 'nodeIdentifier', 'subscriber']), 822 'subscriptions': ('subscriptions', ['sender', 'recipient']), 823 'affiliations': ('affiliations', ['sender', 'recipient']), 824 'create': ('create', ['sender', 'recipient', 'nodeIdentifier']), 825 'getConfigurationOptions': ('getConfigurationOptions', []), 826 'default': ('getDefaultConfiguration', 827 ['sender', 'recipient', 'nodeType']), 828 'configureGet': ('getConfiguration', ['sender', 'recipient', 829 'nodeIdentifier']), 830 'configureSet': ('setConfiguration', ['sender', 'recipient', 831 'nodeIdentifier', 'options']), 832 'items': ('items', ['sender', 'recipient', 'nodeIdentifier', 833 'maxItems', 'itemIdentifiers']), 834 'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier', 835 'itemIdentifiers']), 836 'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']), 837 'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']), 838 } 839 840 hideNodes = False 841 842 def __init__(self, resource=None): 843 self.resource = resource 521 844 self.discoIdentity = {'category': 'pubsub', 522 845 'type': 'generic', … … 527 850 528 851 def connectionMade(self): 529 self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest) 530 self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest) 531 self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest) 532 self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest) 852 self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest) 533 853 534 854 535 855 def getDiscoInfo(self, requestor, target, nodeIdentifier): 536 info = [] 537 538 if not nodeIdentifier: 539 info.append(disco.DiscoIdentity(**self.discoIdentity)) 540 541 info.append(disco.DiscoFeature(disco.NS_ITEMS)) 542 info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature)) 543 for feature in self.pubSubFeatures]) 544 545 def toInfo(nodeInfo): 856 def toInfo(nodeInfo, info): 546 857 if not nodeInfo: 547 return 858 return info 548 859 549 860 (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data'] … … 563 874 form.addField(data_form.Field.fromDict(metaDatum)) 564 875 565 info.append(form.toElement()) 566 567 d = self.getNodeInfo(requestor, target, nodeIdentifier or '') 568 d.addCallback(toInfo) 569 d.addBoth(lambda result: info) 876 info.append(form) 877 878 return info 879 880 info = [] 881 882 request = PubSubRequest('discoInfo') 883 884 if self.resource is not None: 885 resource = self.resource.locateResource(request) 886 identity = resource.discoIdentity 887 features = resource.features 888 getInfo = resource.getInfo 889 else: 890 category, idType, name = self.discoIdentity 891 identity = disco.DiscoIdentity(category, idType, name) 892 features = self.pubSubFeatures 893 getInfo = self.getNodeInfo 894 895 if not nodeIdentifier: 896 info.append(identity) 897 info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS)) 898 info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature)) 899 for feature in features]) 900 901 d = getInfo(requestor, target, nodeIdentifier or '') 902 d.addCallback(toInfo, info) 903 d.addErrback(log.err) 570 904 return d 571 905 572 906 573 907 def getDiscoItems(self, requestor, target, nodeIdentifier): 574 if nodeIdentifier or self.hideNodes: 575 return defer.succeed([]) 576 577 d = self.getNodes(requestor, target) 908 if self.hideNodes: 909 d = defer.succeed([]) 910 elif self.resource is not None: 911 request = PubSubRequest('discoInfo') 912 resource = self.resource.locateResource(request) 913 d = resource.getNodes(requestor, target, nodeIdentifier) 914 elif nodeIdentifier: 915 d = self.getNodes(requestor, target) 916 else: 917 d = defer.succeed([]) 918 919 920 578 921 d.addCallback(lambda nodes: [disco.DiscoItem(target, node) 579 922 for node in nodes]) … … 581 924 582 925 583 def _findForm(self, element, formNamespace): 584 if not element: 585 return None 586 587 form = None 588 for child in element.elements(): 926 def _onPubSubRequest(self, iq): 927 request = PubSubRequest.fromElement(iq) 928 929 if self.resource is not None: 930 resource = self.resource.locateResource(request) 931 else: 932 resource = self 933 934 # Preprocess the request, knowing the handling resource 935 try: 936 preProcessor = getattr(self, '_preProcess_%s' % request.verb) 937 except AttributeError: 938 pass 939 else: 940 request = preProcessor(resource, request) 941 if request is None: 942 return defer.succeed(None) 943 944 # Process the request itself, 945 if resource is not self: 589 946 try: 590 form = data_form.Form.fromElement(child) 591 except data_form.Error: 592 continue 593 594 if form.formNamespace != NS_PUBSUB_NODE_CONFIG: 595 continue 596 597 return form 598 599 600 def _getParameter_node(self, commandElement): 947 handler = getattr(resource, request.verb) 948 except AttributeError: 949 # fix lookup feature 950 text = "Request verb: %s" % request.verb 951 return defer.fail(Unsupported('', text)) 952 953 d = handler(request) 954 else: 955 handlerName, argNames = self._legacyHandlers[request.verb] 956 handler = getattr(self, handlerName) 957 args = [getattr(request, arg) for arg in argNames] 958 d = handler(*args) 959 960 # If needed, translate the result into a response 601 961 try: 602 return commandElement["node"] 603 except KeyError: 604 raise BadRequest('nodeid-required') 605 606 607 def _getParameter_nodeOrEmpty(self, commandElement): 608 return commandElement.getAttribute("node", '') 609 610 611 def _getParameter_jid(self, commandElement): 612 try: 613 return jid.internJID(commandElement["jid"]) 614 except KeyError: 615 raise BadRequest('jid-required') 616 617 618 def _getParameter_max_items(self, commandElement): 619 value = commandElement.getAttribute('max_items') 620 621 if value: 622 try: 623 return int(value) 624 except ValueError: 625 raise BadRequest(text="Field max_items requires a positive " + 626 "integer value") 962 cb = getattr(self, '_toResponse_%s' % request.verb) 963 except AttributeError: 964 pass 965 else: 966 d.addCallback(cb, resource, request) 967 968 return d 969 970 971 def _toResponse_subscribe(self, result, resource, request): 972 response = domish.Element((NS_PUBSUB, "pubsub")) 973 subscription = response.addElement("subscription") 974 if result.nodeIdentifier: 975 subscription["node"] = result.nodeIdentifier 976 subscription["jid"] = result.subscriber.full() 977 subscription["subscription"] = result.state 978 return response 979 980 981 def _toResponse_subscriptions(self, result, resource, request): 982 response = domish.Element((NS_PUBSUB, 'pubsub')) 983 subscriptions = response.addElement('subscriptions') 984 for subscription in result: 985 item = subscriptions.addElement('subscription') 986 item['node'] = subscription.nodeIdentifier 987 item['jid'] = subscription.subscriber.full() 988 item['subscription'] = subscription.state 989 return response 990 991 992 def _toResponse_affiliations(self, result, resource, request): 993 response = domish.Element((NS_PUBSUB, 'pubsub')) 994 affiliations = response.addElement('affiliations') 995 996 for nodeIdentifier, affiliation in result: 997 item = affiliations.addElement('affiliation') 998 item['node'] = nodeIdentifier 999 item['affiliation'] = affiliation 1000 1001 return response 1002 1003 1004 def _toResponse_create(self, result, resource, request): 1005 if not request.nodeIdentifier or request.nodeIdentifier != result: 1006 response = domish.Element((NS_PUBSUB, 'pubsub')) 1007 create = response.addElement('create') 1008 create['node'] = result 1009 return response 627 1010 else: 628 1011 return None 629 630 631 def _getParameters(self, iq, *names):632 requestor = jid.internJID(iq["from"]).userhostJID()633 service = jid.internJID(iq["to"])634 635 params = [requestor, service]636 637 if names:638 command = names[0]639 commandElement = getattr(iq.pubsub, command)640 if not commandElement:641 raise Exception("Could not find command element %r" % command)642 643 for name in names[1:]:644 try:645 getter = getattr(self, '_getParameter_' + name)646 except KeyError:647 raise Exception("No parameter getter for this name")648 649 params.append(getter(commandElement))650 651 return params652 653 654 def _onPublish(self, iq):655 requestor, service, nodeIdentifier = self._getParameters(656 iq, 'publish', 'node')657 658 items = []659 for element in iq.pubsub.publish.elements():660 if element.uri == NS_PUBSUB and element.name == 'item':661 items.append(element)662 663 return self.publish(requestor, service, nodeIdentifier, items)664 665 666 def _onSubscribe(self, iq):667 requestor, service, nodeIdentifier, subscriber = self._getParameters(668 iq, 'subscribe', 'nodeOrEmpty', 'jid')669 670 def toResponse(result):671 response = domish.Element((NS_PUBSUB, "pubsub"))672 subscription = response.addElement("subscription")673 if result.nodeIdentifier:674 subscription["node"] = result.nodeIdentifier675 subscription["jid"] = result.subscriber.full()676 subscription["subscription"] = result.state677 return response678 679 d = self.subscribe(requestor, service, nodeIdentifier, subscriber)680 d.addCallback(toResponse)681 return d682 683 684 def _onUnsubscribe(self, iq):685 requestor, service, nodeIdentifier, subscriber = self._getParameters(686 iq, 'unsubscribe', 'nodeOrEmpty', 'jid')687 688 return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)689 690 691 def _onOptionsGet(self, iq):692 raise Unsupported('subscription-options')693 694 695 def _onOptionsSet(self, iq):696 raise Unsupported('subscription-options')697 698 699 def _onSubscriptions(self, iq):700 requestor, service = self._getParameters(iq)701 702 def toResponse(result):703 response = domish.Element((NS_PUBSUB, 'pubsub'))704 subscriptions = response.addElement('subscriptions')705 for node, subscriber, state in result:706 item = subscriptions.addElement('subscription')707 item['node'] = node708 item['jid'] = subscriber.full()709 item['subscription'] = state710 return response711 712 d = self.subscriptions(requestor, service)713 d.addCallback(toResponse)714 return d715 716 717 def _onAffiliations(self, iq):718 requestor, service = self._getParameters(iq)719 720 def toResponse(result):721 response = domish.Element((NS_PUBSUB, 'pubsub'))722 affiliations = response.addElement('affiliations')723 724 for nodeIdentifier, affiliation in result:725 item = affiliations.addElement('affiliation')726 item['node'] = nodeIdentifier727 item['affiliation'] = affiliation728 729 return response730 731 d = self.affiliations(requestor, service)732 d.addCallback(toResponse)733 return d734 735 736 def _onCreate(self, iq):737 requestor, service = self._getParameters(iq)738 nodeIdentifier = iq.pubsub.create.getAttribute("node")739 740 def toResponse(result):741 if not nodeIdentifier or nodeIdentifier != result:742 response = domish.Element((NS_PUBSUB, 'pubsub'))743 create = response.addElement('create')744 create['node'] = result745 return response746 else:747 return None748 749 d = self.create(requestor, service, nodeIdentifier)750 d.addCallback(toResponse)751 return d752 1012 753 1013 … … 767 1027 return fields 768 1028 769 def _formFromConfiguration(self, values): 770 options = self.getConfigurationOptions() 1029 1030 def _formFromConfiguration(self, resource, values): 1031 options = resource.getConfigurationOptions() 771 1032 fields = self._makeFields(options, values) 772 1033 form = data_form.Form(formType="form", … … 776 1037 return form 777 1038 778 def _checkConfiguration(self, values): 779 options = self.getConfigurationOptions() 1039 1040 def _checkConfiguration(self, resource, values): 1041 options = resource.getConfigurationOptions() 780 1042 processedValues = {} 781 1043 … … 801 1063 802 1064 803 def _onDefault(self, iq): 804 requestor, service = self._getParameters(iq) 805 806 def toResponse(options): 807 response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) 808 default = response.addElement("default") 809 default.addChild(self._formFromConfiguration(options).toElement()) 810 return response 811 812 form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG) 813 values = form and form.formType == 'result' and form.getValues() or {} 814 nodeType = values.get('pubsub#node_type', 'leaf') 815 816 if nodeType not in ('leaf', 'collections'): 817 return defer.fail(error.StanzaError('not-acceptable')) 818 819 d = self.getDefaultConfiguration(requestor, service, nodeType) 820 d.addCallback(toResponse) 821 return d 822 823 824 def _onConfigureGet(self, iq): 825 requestor, service, nodeIdentifier = self._getParameters( 826 iq, 'configure', 'nodeOrEmpty') 827 828 def toResponse(options): 829 response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) 830 configure = response.addElement("configure") 831 configure.addChild(self._formFromConfiguration(options).toElement()) 832 833 if nodeIdentifier: 834 configure["node"] = nodeIdentifier 835 836 return response 837 838 d = self.getConfiguration(requestor, service, nodeIdentifier) 839 d.addCallback(toResponse) 840 return d 841 842 843 def _onConfigureSet(self, iq): 844 requestor, service, nodeIdentifier = self._getParameters( 845 iq, 'configure', 'nodeOrEmpty') 846 847 # Search configuration form with correct FORM_TYPE and process it 848 849 form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG) 850 851 if form: 852 if form.formType == 'submit': 853 options = self._checkConfiguration(form.getValues()) 854 855 return self.setConfiguration(requestor, service, 856 nodeIdentifier, options) 857 elif form.formType == 'cancel': 858 return None 859 860 raise BadRequest() 861 862 863 def _onItems(self, iq): 864 requestor, service, nodeIdentifier, maxItems = self._getParameters( 865 iq, 'items', 'nodeOrEmpty', 'max_items') 866 867 itemIdentifiers = [] 868 for child in iq.pubsub.items.elements(): 869 if child.name == 'item' and child.uri == NS_PUBSUB: 870 try: 871 itemIdentifiers.append(child["id"]) 872 except KeyError: 873 raise BadRequest() 874 875 def toResponse(result): 876 response = domish.Element((NS_PUBSUB, 'pubsub')) 877 items = response.addElement('items') 878 if nodeIdentifier: 879 items["node"] = nodeIdentifier 880 881 for item in result: 882 items.addChild(item) 883 884 return response 885 886 d = self.items(requestor, service, nodeIdentifier, maxItems, 887 itemIdentifiers) 888 d.addCallback(toResponse) 889 return d 890 891 892 def _onRetract(self, iq): 893 requestor, service, nodeIdentifier = self._getParameters( 894 iq, 'retract', 'node') 895 896 itemIdentifiers = [] 897 for child in iq.pubsub.retract.elements(): 898 if child.uri == NS_PUBSUB and child.name == 'item': 899 try: 900 itemIdentifiers.append(child["id"]) 901 except KeyError: 902 raise BadRequest() 903 904 return self.retract(requestor, service, nodeIdentifier, 905 itemIdentifiers) 906 907 908 def _onPurge(self, iq): 909 requestor, service, nodeIdentifier = self._getParameters( 910 iq, 'purge', 'node') 911 return self.purge(requestor, service, nodeIdentifier) 912 913 914 def _onDelete(self, iq): 915 requestor, service, nodeIdentifier = self._getParameters( 916 iq, 'delete', 'node') 917 return self.delete(requestor, service, nodeIdentifier) 918 919 920 def _onAffiliationsGet(self, iq): 921 raise Unsupported('modify-affiliations') 922 923 924 def _onAffiliationsSet(self, iq): 925 raise Unsupported('modify-affiliations') 926 927 928 def _onSubscriptionsGet(self, iq): 929 raise Unsupported('manage-subscriptions') 930 931 932 def _onSubscriptionsSet(self, iq): 933 raise Unsupported('manage-subscriptions') 934 935 # public methods 1065 def _preProcess_default(self, resource, request): 1066 if request.nodeType not in ('leaf', 'collection'): 1067 raise error.StanzaError('not-acceptable') 1068 else: 1069 return request 1070 1071 1072 def _toResponse_default(self, options, resource, request): 1073 response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) 1074 default = response.addElement("default") 1075 form = self._formFromConfiguration(resource, options) 1076 default.addChild(form.toElement()) 1077 return response 1078 1079 1080 def _toResponse_configureGet(self, options, resource, request): 1081 response = domish.Element((NS_PUBSUB_OWNER, "pubsub")) 1082 configure = response.addElement("configure") 1083 form = self._formFromConfiguration(resource, options) 1084 configure.addChild(form.toElement()) 1085 1086 if request.nodeIdentifier: 1087 configure["node"] = request.nodeIdentifier 1088 1089 return response 1090 1091 1092 def _preProcess_configureSet(self, resource, request): 1093 if request.options: 1094 request.options = self._checkConfiguration(resource, 1095 request.options) 1096 return request 1097 else: 1098 return None 1099 1100 1101 def _toResponse_items(self, result, resource, request): 1102 response = domish.Element((NS_PUBSUB, 'pubsub')) 1103 items = response.addElement('items') 1104 items["node"] = request.nodeIdentifier 1105 1106 for item in result: 1107 items.addChild(item) 1108 1109 return response 1110 936 1111 937 1112 def _createNotification(self, eventType, service, nodeIdentifier, … … 956 1131 957 1132 return message 1133 1134 # public methods 958 1135 959 1136 def notifyPublish(self, service, nodeIdentifier, notifications): … … 966 1143 967 1144 968 def notifyDelete(self, service, nodeIdentifier, subscriptions): 969 for subscription in subscriptions: 1145 def notifyDelete(self, service, nodeIdentifier, subscribers, 1146 redirectURI=None): 1147 for subscriber in subscribers: 970 1148 message = self._createNotification('delete', service, 971 1149 nodeIdentifier, 972 subscription.subscriber) 1150 subscriber) 1151 if redirectURI: 1152 redirect = message.event.delete.addElement('redirect') 1153 redirect['uri'] = redirectURI 973 1154 self.send(message) 974 1155 … … 1010 1191 1011 1192 1012 def getDefaultConfiguration(self, requestor, service ):1193 def getDefaultConfiguration(self, requestor, service, nodeType): 1013 1194 raise Unsupported('retrieve-default') 1014 1195 … … 1037 1218 def delete(self, requestor, service, nodeIdentifier): 1038 1219 raise Unsupported('delete-nodes') 1220 1221 1222 1223 class PubSubResource(object): 1224 1225 implements(IPubSubResource) 1226 1227 features = [] 1228 discoIdentity = disco.DiscoIdentity('pubsub', 1229 'service', 1230 'Publish-Subscribe Service') 1231 1232 1233 def locateResource(self, request): 1234 return self 1235 1236 1237 def getInfo(self, requestor, service, nodeIdentifier): 1238 return defer.succeed(None) 1239 1240 1241 def getNodes(self, requestor, service, nodeIdentifier): 1242 return defer.succeed([]) 1243 1244 1245 def getConfigurationOptions(self): 1246 return {} 1247 1248 1249 def publish(self, request): 1250 return defer.fail(Unsupported('publish')) 1251 1252 1253 def subscribe(self, request): 1254 return defer.fail(Unsupported('subscribe')) 1255 1256 1257 def unsubscribe(self, request): 1258 return defer.fail(Unsupported('subscribe')) 1259 1260 1261 def subscriptions(self, request): 1262 return defer.fail(Unsupported('retrieve-subscriptions')) 1263 1264 1265 def affiliations(self, request): 1266 return defer.fail(Unsupported('retrieve-affiliations')) 1267 1268 1269 def create(self, request): 1270 return defer.fail(Unsupported('create-nodes')) 1271 1272 1273 def default(self, request): 1274 return defer.fail(Unsupported('retrieve-default')) 1275 1276 1277 def configureGet(self, request): 1278 return defer.fail(Unsupported('config-node')) 1279 1280 1281 def configureSet(self, request): 1282 return defer.fail(Unsupported('config-node')) 1283 1284 1285 def items(self, request): 1286 return defer.fail(Unsupported('retrieve-items')) 1287 1288 1289 def retract(self, request): 1290 return defer.fail(Unsupported('retract-items')) 1291 1292 1293 def purge(self, request): 1294 return defer.fail(Unsupported('purge-nodes')) 1295 1296 1297 def delete(self, request): 1298 return defer.fail(Unsupported('delete-nodes')) 1299 1300 1301 def affiliationsGet(self, request): 1302 return defer.fail(Unsupported('modify-affiliations')) 1303 1304 1305 def affiliationsSet(self, request): 1306 return defer.fail(Unsupported('modify-affiliations')) 1307 1308 1309 def subscriptionsGet(self, request): 1310 return defer.fail(Unsupported('manage-subscriptions')) 1311 1312 1313 def subscriptionsSet(self, request): 1314 return defer.fail(Unsupported('manage-subscriptions')) -
wokkel/subprotocols.py
r22 r53 1 1 # -*- test-case-name: wokkel.test.test_subprotocols -*- 2 2 # 3 # Copyright (c) 2001-200 7Twisted Matrix Laboratories.3 # Copyright (c) 2001-2009 Twisted Matrix Laboratories. 4 4 # See LICENSE for details. 5 5 … … 13 13 from twisted.python import log 14 14 from twisted.words.protocols.jabber import error, xmlstream 15 from twisted.words.protocols.jabber.xmlstream import toResponse 15 16 from twisted.words.xish import xpath 16 17 from twisted.words.xish.domish import IElement 17 18 18 try:19 from twisted.words.protocols.jabber.xmlstream import toResponse20 except ImportError:21 from wokkel.compat import toResponse22 23 19 from wokkel.iwokkel import IXMPPHandler, IXMPPHandlerCollection 24 20 25 21 class XMPPHandler(object): 22 """ 23 XMPP protocol handler. 24 25 Classes derived from this class implement (part of) one or more XMPP 26 extension protocols, and are referred to as a subprotocol implementation. 27 """ 28 26 29 implements(IXMPPHandler) 30 31 def __init__(self): 32 self.parent = None 33 self.xmlstream = None 34 27 35 28 36 def setHandlerParent(self, parent): … … 30 38 self.parent.addHandler(self) 31 39 40 32 41 def disownHandlerParent(self, parent): 33 42 self.parent.removeHandler(self) 34 43 self.parent = None 35 44 45 36 46 def makeConnection(self, xs): 37 47 self.xmlstream = xs 38 48 self.connectionMade() 39 49 50 40 51 def connectionMade(self): 41 pass 52 """ 53 Called after a connection has been established. 54 55 Can be overridden to perform work before stream initialization. 56 """ 57 42 58 43 59 def connectionInitialized(self): 44 pass 60 """ 61 The XML stream has been initialized. 62 63 Can be overridden to perform work after stream initialization, e.g. to 64 set up observers and start exchanging XML stanzas. 65 """ 66 45 67 46 68 def connectionLost(self, reason): 69 """ 70 The XML stream has been closed. 71 72 This method can be extended to inspect the C{reason} argument and 73 act on it. 74 """ 47 75 self.xmlstream = None 76 48 77 49 78 def send(self, obj): … … 65 94 66 95 96 67 97 class XMPPHandlerCollection(object): 68 98 """ … … 72 102 L{XMPPHandler} itself, so this is not recursive. 73 103 74 @ivar xmlstream: Currently managed XML stream.75 @type xmlstream: L{XmlStream}76 104 @ivar handlers: List of protocol handlers. 77 105 @type handlers: L{list} of objects providing … … 83 111 def __init__(self): 84 112 self.handlers = [] 85 self.xmlstream = None 86 self._initialized = False 113 87 114 88 115 def __iter__(self): … … 92 119 return iter(self.handlers) 93 120 121 94 122 def addHandler(self, handler): 95 123 """ … … 97 125 98 126 Protocol handlers are expected to provide L{IXMPPHandler}. 99 100 When an XML stream has already been established, the handler's 101 C{connectionInitialized} will be called to get it up to speed. 102 """ 103 127 """ 104 128 self.handlers.append(handler) 105 129 106 # get protocol handler up to speed when a connection has already107 # been established108 if self.xmlstream and self._initialized:109 handler.makeConnection(self.xmlstream)110 handler.connectionInitialized()111 130 112 131 def removeHandler(self, handler): … … 114 133 Remove protocol handler. 115 134 """ 116 117 135 self.handlers.remove(handler) 136 137 118 138 119 139 class StreamManager(XMPPHandlerCollection): … … 126 146 using L{addHandler}. 127 147 148 @ivar xmlstream: currently managed XML stream 149 @type xmlstream: L{XmlStream} 128 150 @ivar logTraffic: if true, log all traffic. 129 151 @type logTraffic: L{bool} 152 @ivar _initialized: Whether the stream represented by L{xmlstream} has 153 been initialized. This is used when caching outgoing 154 stanzas. 155 @type _initialized: C{bool} 130 156 @ivar _packetQueue: internal buffer of unsent data. See L{send} for details. 131 157 @type _packetQueue: L{list} … … 135 161 136 162 def __init__(self, factory): 137 self.handlers = []163 XMPPHandlerCollection.__init__(self) 138 164 self.xmlstream = None 139 165 self._packetQueue = [] … … 147 173 self.factory = factory 148 174 175 176 def addHandler(self, handler): 177 """ 178 Add protocol handler. 179 180 When an XML stream has already been established, the handler's 181 C{connectionInitialized} will be called to get it up to speed. 182 """ 183 XMPPHandlerCollection.addHandler(self, handler) 184 185 # get protocol handler up to speed when a connection has already 186 # been established 187 if self.xmlstream and self._initialized: 188 handler.makeConnection(self.xmlstream) 189 handler.connectionInitialized() 190 191 149 192 def _connected(self, xs): 193 """ 194 Called when the transport connection has been established. 195 196 Here we optionally set up traffic logging (depending on L{logTraffic}) 197 and call each handler's C{makeConnection} method with the L{XmlStream} 198 instance. 199 """ 150 200 def logDataIn(buf): 151 201 log.msg("RECV: %r" % buf) … … 163 213 e.makeConnection(xs) 164 214 215 165 216 def _authd(self, xs): 217 """ 218 Called when the stream has been initialized. 219 220 Send out cached stanzas and call each handler's 221 C{connectionInitialized} method. 222 """ 166 223 # Flush all pending packets 167 224 for p in self._packetQueue: … … 175 232 e.connectionInitialized() 176 233 234 177 235 def initializationFailed(self, reason): 178 236 """ … … 188 246 """ 189 247 248 190 249 def _disconnected(self, _): 250 """ 251 Called when the stream has been closed. 252 253 From this point on, the manager doesn't interact with the 254 L{XmlStream} anymore and notifies each handler that the connection 255 was lost by calling its C{connectionLost} method. 256 """ 191 257 self.xmlstream = None 192 258 self._initialized = False … … 195 261 # the IService interface 196 262 for e in self: 197 e.xmlstream = None198 263 e.connectionLost(None) 264 199 265 200 266 def send(self, obj): … … 208 274 L{xmlstream.XmlStream.send} for details. 209 275 """ 210 211 276 if self._initialized: 212 277 self.xmlstream.send(obj) 213 278 else: 214 279 self._packetQueue.append(obj) 280 215 281 216 282 -
wokkel/test/helpers.py
r10 r46 6 6 """ 7 7 8 from twisted.internet import defer 9 from twisted.words.xish import xpath 8 10 from twisted.words.xish.utility import EventDispatcher 11 12 from wokkel.generic import parseXml 9 13 10 14 class XmlStreamStub(object): … … 55 59 """ 56 60 self.xmlstream.dispatch(obj) 61 62 63 class TestableRequestHandlerMixin(object): 64 """ 65 Mixin for testing XMPPHandlers that process iq requests. 66 67 Handlers that use L{wokkel.subprotocols.IQHandlerMixin} define a 68 C{iqHandlers} attribute that lists the handlers to be called for iq 69 requests. This mixin provides L{handleRequest} to mimic the handler 70 processing for easier testing. 71 """ 72 73 def handleRequest(self, xml): 74 """ 75 Find a handler and call it directly. 76 77 @param xml: XML stanza that may yield a handler being called. 78 @type xml: C{str}. 79 @return: Deferred that fires with the result of a handler for this 80 stanza. If no handler was found, the deferred has its errback 81 called with a C{NotImplementedError} exception. 82 """ 83 handler = None 84 iq = parseXml(xml) 85 for queryString, method in self.service.iqHandlers.iteritems(): 86 if xpath.internQuery(queryString).matches(iq): 87 handler = getattr(self.service, method) 88 89 if handler: 90 d = defer.maybeDeferred(handler, iq) 91 else: 92 d = defer.fail(NotImplementedError()) 93 94 return d -
wokkel/test/test_client.py
r8 r42 6 6 """ 7 7 8 from twisted.internet import defer 8 9 from twisted.trial import unittest 10 from twisted.words.protocols.jabber import xmlstream 11 from twisted.words.protocols.jabber.client import XMPPAuthenticator 9 12 from twisted.words.protocols.jabber.jid import JID 10 13 from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT 11 14 from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT 12 15 13 from wokkel.client import DeferredClientFactory 16 from wokkel import client 17 from wokkel.test.test_compat import BootstrapMixinTest 14 18 15 class DeferredClientFactoryTest(unittest.TestCase): 19 class DeferredClientFactoryTest(BootstrapMixinTest): 20 """ 21 Tests for L{client.DeferredClientFactory}. 22 """ 23 24 def setUp(self): 25 self.factory = client.DeferredClientFactory(JID('user@example.org'), 26 'secret') 27 28 29 def test_buildProtocol(self): 30 """ 31 The authenticator factory should be passed to its protocol and it 32 should instantiate the authenticator and save it. 33 L{xmlstream.XmlStream}s do that, but we also want to ensure it really 34 is one. 35 """ 36 xs = self.factory.buildProtocol(None) 37 self.assertIdentical(self.factory, xs.factory) 38 self.assertIsInstance(xs, xmlstream.XmlStream) 39 self.assertIsInstance(xs.authenticator, XMPPAuthenticator) 40 16 41 17 42 def test_deferredOnInitialized(self): … … 20 45 """ 21 46 22 f = DeferredClientFactory(JID('user@example.org'), 'secret')23 x mlstream = f.buildProtocol(None)24 xmlstream.dispatch(xmlstream, STREAM_AUTHD_EVENT)25 return f.deferred 47 xs = self.factory.buildProtocol(None) 48 xs.dispatch(xs, STREAM_AUTHD_EVENT) 49 return self.factory.deferred 50 26 51 27 52 def test_deferredOnNotInitialized(self): … … 30 55 """ 31 56 32 f = DeferredClientFactory(JID('user@example.org'), 'secret')33 xmlstream = f.buildProtocol(None)34 35 57 class TestException(Exception): 36 58 pass 37 59 38 xmlstream.dispatch(TestException(), INIT_FAILED_EVENT) 39 self.assertFailure(f.deferred, TestException) 40 return f.deferred 60 xs = self.factory.buildProtocol(None) 61 xs.dispatch(TestException(), INIT_FAILED_EVENT) 62 self.assertFailure(self.factory.deferred, TestException) 63 return self.factory.deferred 64 41 65 42 66 def test_deferredOnConnectionFailure(self): … … 45 69 """ 46 70 47 f = DeferredClientFactory(JID('user@example.org'), 'secret')48 xmlstream = f.buildProtocol(None)49 50 71 class TestException(Exception): 51 72 pass 52 73 53 f.clientConnectionFailed(self, TestException()) 54 self.assertFailure(f.deferred, TestException) 55 return f.deferred 74 xs = self.factory.buildProtocol(None) 75 self.factory.clientConnectionFailed(self, TestException()) 76 self.assertFailure(self.factory.deferred, TestException) 77 return self.factory.deferred 78 79 80 81 class ClientCreatorTest(unittest.TestCase): 82 """ 83 Tests for L{client.clientCreator}. 84 """ 85 86 def test_call(self): 87 """ 88 The factory is passed to an SRVConnector and a connection initiated. 89 """ 90 91 d1 = defer.Deferred() 92 factory = client.DeferredClientFactory(JID('user@example.org'), 93 'secret') 94 95 def cb(connector): 96 self.assertEqual('xmpp-client', connector.service) 97 self.assertEqual('example.org', connector.domain) 98 self.assertEqual(factory, connector.factory) 99 100 def connect(connector): 101 d1.callback(connector) 102 103 d1.addCallback(cb) 104 self.patch(client.SRVConnector, 'connect', connect) 105 106 d2 = client.clientCreator(factory) 107 self.assertEqual(factory.deferred, d2) 108 109 return d1 -
wokkel/test/test_compat.py
r8 r53 1 # Copyright (c) 2001-200 7Twisted Matrix Laboratories.2 # Copyright (c) 2008 Ralph Meijer1 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. 2 # Copyright (c) 2008-2009 Ralph Meijer 3 3 # See LICENSE for details. 4 4 … … 7 7 """ 8 8 9 from zope.interface.verify import verifyObject 9 10 from twisted.internet import defer, protocol 11 from twisted.internet.interfaces import IProtocolFactory 10 12 from twisted.trial import unittest 11 13 from twisted.words.xish import domish, utility 12 from wokkel.compat import toResponse, XmlStreamFactoryMixin 14 from twisted.words.protocols.jabber import xmlstream 15 from wokkel.compat import BootstrapMixin, XmlStreamServerFactory 13 16 14 17 class DummyProtocol(protocol.Protocol, utility.EventDispatcher): … … 16 19 I am a protocol with an event dispatcher without further processing. 17 20 18 This protocol is only used for testing XmlStreamFactoryMixin to make21 This protocol is only used for testing BootstrapMixin to make 19 22 sure the bootstrap observers are added to the protocol instance. 20 23 """ … … 28 31 29 32 30 class XmlStreamFactoryMixinTest(unittest.TestCase):31 33 32 def test_buildProtocol(self): 34 class BootstrapMixinTest(unittest.TestCase): 35 """ 36 Tests for L{BootstrapMixin}. 37 38 @ivar factory: Instance of the factory or mixin under test. 39 """ 40 41 def setUp(self): 42 self.factory = BootstrapMixin() 43 44 45 def test_installBootstraps(self): 33 46 """ 34 Test building of protocol. 47 Dispatching an event should fire registered bootstrap observers. 48 """ 49 called = [] 35 50 36 Arguments passed to Factory should be passed to protocol on 37 instantiation. Bootstrap observers should be setup. 38 """ 39 d = defer.Deferred() 51 def cb(data): 52 called.append(data) 40 53 41 f = XmlStreamFactoryMixin(None, test=None) 42 f.protocol = DummyProtocol 43 f.addBootstrap('//event/myevent', d.callback) 44 xs = f.buildProtocol(None) 54 dispatcher = DummyProtocol() 55 self.factory.addBootstrap('//event/myevent', cb) 56 self.factory.installBootstraps(dispatcher) 45 57 46 self.assertEquals(f, xs.factory) 47 self.assertEquals((None,), xs.args) 48 self.assertEquals({'test': None}, xs.kwargs) 49 xs.dispatch(None, '//event/myevent') 50 return d 58 dispatcher.dispatch(None, '//event/myevent') 59 self.assertEquals(1, len(called)) 60 51 61 52 62 def test_addAndRemoveBootstrap(self): … … 54 64 Test addition and removal of a bootstrap event handler. 55 65 """ 56 def cb(self):57 pass58 66 59 f = XmlStreamFactoryMixin(None, test=None)67 called = [] 60 68 61 f.addBootstrap('//event/myevent', cb)62 self.assertIn(('//event/myevent', cb), f.bootstraps)69 def cb(data): 70 called.append(data) 63 71 64 f.removeBootstrap('//event/myevent', cb)65 self. assertNotIn(('//event/myevent', cb), f.bootstraps)72 self.factory.addBootstrap('//event/myevent', cb) 73 self.factory.removeBootstrap('//event/myevent', cb) 66 74 67 class ToResponseTest(unittest.TestCase): 75 dispatcher = DummyProtocol() 76 self.factory.installBootstraps(dispatcher) 68 77 69 def test_toResponse(self): 78 dispatcher.dispatch(None, '//event/myevent') 79 self.assertFalse(called) 80 81 82 83 class XmlStreamServerFactoryTest(BootstrapMixinTest): 84 """ 85 Tests for L{XmlStreamServerFactory}. 86 """ 87 88 def setUp(self): 70 89 """ 71 Test that a response stanza is generated with addressing swapped.90 Set up a server factory with a authenticator factory function. 72 91 """ 73 stanza = domish.Element(('jabber:client', 'iq')) 74 stanza['type'] = 'get' 75 stanza['to'] = 'user1@example.com' 76 stanza['from'] = 'user2@example.com/resource' 77 stanza['id'] = 'stanza1' 78 response = toResponse(stanza, 'result') 79 self.assertNotIdentical(stanza, response) 80 self.assertEqual(response['from'], 'user1@example.com') 81 self.assertEqual(response['to'], 'user2@example.com/resource') 82 self.assertEqual(response['type'], 'result') 83 self.assertEqual(response['id'], 'stanza1') 92 class TestAuthenticator(object): 93 def __init__(self): 94 self.xmlstreams = [] 84 95 85 def test_toResponseNoFrom(self): 96 def associateWithStream(self, xs): 97 self.xmlstreams.append(xs) 98 99 def authenticatorFactory(): 100 return TestAuthenticator() 101 102 self.factory = XmlStreamServerFactory(authenticatorFactory) 103 104 105 def test_interface(self): 86 106 """ 87 Test that a response is generated from a stanza without a from address.107 L{XmlStreamServerFactory} is a L{Factory}. 88 108 """ 89 stanza = domish.Element(('jabber:client', 'iq')) 90 stanza['type'] = 'get' 91 stanza['to'] = 'user1@example.com' 92 response = toResponse(stanza) 93 self.assertEqual(response['from'], 'user1@example.com') 94 self.failIf(response.hasAttribute('to')) 109 verifyObject(IProtocolFactory, self.factory) 95 110 96 def test_toResponseNoTo(self): 111 112 def test_buildProtocolAuthenticatorInstantiation(self): 97 113 """ 98 Test that a response is generated from a stanza without a to address. 114 The authenticator factory should be used to instantiate the 115 authenticator and pass it to the protocol. 116 117 The default protocol, L{XmlStream} stores the authenticator it is 118 passed, and calls its C{associateWithStream} method. so we use that to 119 check whether our authenticator factory is used and the protocol 120 instance gets an authenticator. 99 121 """ 100 stanza = domish.Element(('jabber:client', 'iq')) 101 stanza['type'] = 'get' 102 stanza['from'] = 'user2@example.com/resource' 103 response = toResponse(stanza) 104 self.failIf(response.hasAttribute('from')) 105 self.assertEqual(response['to'], 'user2@example.com/resource') 122 xs = self.factory.buildProtocol(None) 123 self.assertEquals([xs], xs.authenticator.xmlstreams) 106 124 107 def test_toResponseNoAddressing(self): 125 126 def test_buildProtocolXmlStream(self): 108 127 """ 109 T est that a response is generated from a stanza without any addressing.128 The protocol factory creates Jabber XML Stream protocols by default. 110 129 """ 111 stanza = domish.Element(('jabber:client', 'message')) 112 stanza['type'] = 'chat' 113 response = toResponse(stanza) 114 self.failIf(response.hasAttribute('to')) 115 self.failIf(response.hasAttribute('from')) 130 xs = self.factory.buildProtocol(None) 131 self.assertIsInstance(xs, xmlstream.XmlStream) 116 132 117 def test_noID(self): 133 134 def test_buildProtocolTwice(self): 118 135 """ 119 Test that a proper response is generated without id attribute. 136 Subsequent calls to buildProtocol should result in different instances 137 of the protocol, as well as their authenticators. 120 138 """ 121 stanza = domish.Element(('jabber:client', 'message')) 122 response = toResponse(stanza) 123 self.failIf(response.hasAttribute('id')) 139 xs1 = self.factory.buildProtocol(None) 140 xs2 = self.factory.buildProtocol(None) 141 self.assertNotIdentical(xs1, xs2) 142 self.assertNotIdentical(xs1.authenticator, xs2.authenticator) 143 144 145 def test_buildProtocolInstallsBootstraps(self): 146 """ 147 The protocol factory installs bootstrap event handlers on the protocol. 148 """ 149 called = [] 150 151 def cb(data): 152 called.append(data) 153 154 self.factory.addBootstrap('//event/myevent', cb) 155 156 xs = self.factory.buildProtocol(None) 157 xs.dispatch(None, '//event/myevent') 158 159 self.assertEquals(1, len(called)) 160 161 162 def test_buildProtocolStoresFactory(self): 163 """ 164 The protocol factory is saved in the protocol. 165 """ 166 xs = self.factory.buildProtocol(None) 167 self.assertIdentical(self.factory, xs.factory) -
wokkel/test/test_data_form.py
r29 r56 1 # Copyright (c) 2003-200 8Ralph Meijer1 # Copyright (c) 2003-2009 Ralph Meijer 2 2 # See LICENSE for details. 3 3 … … 8 8 from twisted.trial import unittest 9 9 from twisted.words.xish import domish 10 11 from wokkel.data_form import Field, Form, Option, FieldNameRequiredError 10 from twisted.words.protocols.jabber import jid 11 12 from wokkel import data_form 12 13 13 14 NS_X_DATA = 'jabber:x:data' 14 15 15 16 17 16 class OptionTest(unittest.TestCase): 18 17 """ 19 Tests for L{ Option}.18 Tests for L{data_form.Option}. 20 19 """ 21 20 22 21 def test_toElement(self): 23 option = Option('value', 'label')22 option = data_form.Option('value', 'label') 24 23 element = option.toElement() 25 24 self.assertEquals('option', element.name) … … 34 33 class FieldTest(unittest.TestCase): 35 34 """ 36 Tests for L{ Field}.35 Tests for L{data_form.Field}. 37 36 """ 38 37 39 38 def test_basic(self): 40 field = Field(var='test') 39 """ 40 Test basic field initialization. 41 """ 42 field = data_form.Field(var='test') 41 43 self.assertEqual('text-single', field.fieldType) 42 44 self.assertEqual('test', field.var) 43 45 46 47 def test_toElement(self): 48 """ 49 Test rendering to a DOM. 50 """ 51 field = data_form.Field(var='test') 44 52 element = field.toElement() 45 53 … … 47 55 self.assertEquals('field', element.name) 48 56 self.assertEquals(NS_X_DATA, element.uri) 49 self.assertEquals('text-single', element['type']) 57 self.assertEquals('text-single', 58 element.getAttribute('type', 'text-single')) 50 59 self.assertEquals('test', element['var']) 51 60 self.assertEquals([], element.children) 52 61 53 62 54 def test_noFieldName(self): 55 field = Field() 56 self.assertRaises(FieldNameRequiredError, field.toElement) 63 def test_toElementTypeNotListSingle(self): 64 """ 65 Always render the field type, if different from list-single. 66 """ 67 field = data_form.Field('hidden', var='test') 68 element = field.toElement() 69 70 self.assertEquals('hidden', element.getAttribute('type')) 71 72 73 def test_toElementAsForm(self): 74 """ 75 Always render the field type, if asForm is True. 76 """ 77 field = data_form.Field(var='test') 78 element = field.toElement(True) 79 80 self.assertEquals('text-single', element.getAttribute('type')) 81 82 83 def test_toElementOptions(self): 84 """ 85 Test rendering to a DOM with options. 86 """ 87 field = data_form.Field('list-single', var='test') 88 field.options = [data_form.Option(u'option1'), 89 data_form.Option(u'option2')] 90 element = field.toElement(True) 91 92 self.assertEqual(2, len(element.children)) 93 94 95 def test_toElementLabel(self): 96 """ 97 Test rendering to a DOM with a label. 98 """ 99 field = data_form.Field(var='test', label=u'my label') 100 element = field.toElement(True) 101 102 self.assertEqual(u'my label', element.getAttribute('label')) 103 104 105 def test_toElementDescription(self): 106 """ 107 Test rendering to a DOM with options. 108 """ 109 field = data_form.Field(var='test', desc=u'My desc') 110 element = field.toElement(True) 111 112 self.assertEqual(1, len(element.children)) 113 child = element.children[0] 114 self.assertEqual('desc', child.name) 115 self.assertEqual(NS_X_DATA, child.uri) 116 self.assertEqual(u'My desc', unicode(child)) 117 118 119 def test_toElementRequired(self): 120 """ 121 Test rendering to a DOM with options. 122 """ 123 field = data_form.Field(var='test', required=True) 124 element = field.toElement(True) 125 126 self.assertEqual(1, len(element.children)) 127 child = element.children[0] 128 self.assertEqual('required', child.name) 129 self.assertEqual(NS_X_DATA, child.uri) 130 131 132 def test_toElementJID(self): 133 field = data_form.Field(fieldType='jid-single', var='test', 134 value=jid.JID(u'test@example.org')) 135 element = field.toElement() 136 self.assertEqual(u'test@example.org', unicode(element.value)) 137 138 139 def test_typeCheckNoFieldName(self): 140 """ 141 A field not of type fixed must have a var. 142 """ 143 field = data_form.Field(fieldType='list-single') 144 self.assertRaises(data_form.FieldNameRequiredError, field.typeCheck) 145 146 147 def test_typeCheckTooManyValues(self): 148 """ 149 Expect an exception if too many values are set, depending on type. 150 """ 151 field = data_form.Field(fieldType='list-single', var='test', 152 values=[u'value1', u'value2']) 153 self.assertRaises(data_form.TooManyValuesError, field.typeCheck) 154 155 156 def test_typeCheckBooleanFalse(self): 157 """ 158 Test possible False values for a boolean field. 159 """ 160 field = data_form.Field(fieldType='boolean', var='test') 161 162 for value in (False, 0, '0', 'false', 'False', []): 163 field.value = value 164 field.typeCheck() 165 self.assertIsInstance(field.value, bool) 166 self.assertFalse(field.value) 167 168 169 def test_typeCheckBooleanTrue(self): 170 """ 171 Test possible True values for a boolean field. 172 """ 173 field = data_form.Field(fieldType='boolean', var='test') 174 175 for value in (True, 1, '1', 'true', 'True', ['test']): 176 field.value = value 177 field.typeCheck() 178 self.assertIsInstance(field.value, bool) 179 self.assertTrue(field.value) 180 181 182 def test_typeCheckBooleanBad(self): 183 """ 184 A bad value for a boolean field should raise a ValueError 185 """ 186 field = data_form.Field(fieldType='boolean', var='test') 187 field.value = 'test' 188 self.assertRaises(ValueError, field.typeCheck) 189 190 191 def test_typeCheckJID(self): 192 """ 193 The value of jid field should be a JID or coercable to one. 194 """ 195 field = data_form.Field(fieldType='jid-single', var='test', 196 value=jid.JID('test@example.org')) 197 field.typeCheck() 198 199 200 def test_typeCheckJIDString(self): 201 """ 202 The string value of jid field should be coercable into a JID. 203 """ 204 field = data_form.Field(fieldType='jid-single', var='test', 205 value='test@example.org') 206 field.typeCheck() 207 self.assertEquals(jid.JID(u'test@example.org'), field.value) 208 209 210 def test_typeCheckJIDBad(self): 211 """ 212 An invalid JID string should raise an exception. 213 """ 214 field = data_form.Field(fieldType='jid-single', var='test', 215 value='test@@example.org') 216 self.assertRaises(jid.InvalidFormat, field.typeCheck) 57 217 58 218 … … 60 220 element = domish.Element((NS_X_DATA, 'field')) 61 221 element['type'] = 'fixed' 62 field = Field.fromElement(element)222 field = data_form.Field.fromElement(element) 63 223 self.assertEquals('fixed', field.fieldType) 64 224 … … 66 226 def test_fromElementNoType(self): 67 227 element = domish.Element((NS_X_DATA, 'field')) 68 field = Field.fromElement(element)228 field = data_form.Field.fromElement(element) 69 229 self.assertEquals(None, field.fieldType) 70 230 71 231 72 def test_fromElementValue(self): 73 element = domish.Element((NS_X_DATA, 'field')) 74 element.addElement("value", content="text") 75 field = Field.fromElement(element) 232 def test_fromElementValueTextSingle(self): 233 """ 234 Parsed text-single field values should be of type C{unicode}. 235 """ 236 element = domish.Element((NS_X_DATA, 'field')) 237 element['type'] = 'text-single' 238 element.addElement('value', content=u'text') 239 field = data_form.Field.fromElement(element) 76 240 self.assertEquals('text', field.value) 77 241 78 242 243 def test_fromElementValueJID(self): 244 """ 245 Parsed jid-single field values should be of type C{unicode}. 246 """ 247 element = domish.Element((NS_X_DATA, 'field')) 248 element['type'] = 'jid-single' 249 element.addElement('value', content=u'user@example.org') 250 field = data_form.Field.fromElement(element) 251 self.assertEquals(u'user@example.org', field.value) 252 253 def test_fromElementValueJIDMalformed(self): 254 """ 255 Parsed jid-single field values should be of type C{unicode}. 256 257 No validation should be done at this point, so invalid JIDs should 258 also be passed as-is. 259 """ 260 element = domish.Element((NS_X_DATA, 'field')) 261 element['type'] = 'jid-single' 262 element.addElement('value', content=u'@@') 263 field = data_form.Field.fromElement(element) 264 self.assertEquals(u'@@', field.value) 265 266 267 def test_fromElementValueBoolean(self): 268 """ 269 Parsed boolean field values should be of type C{unicode}. 270 """ 271 element = domish.Element((NS_X_DATA, 'field')) 272 element['type'] = 'boolean' 273 element.addElement('value', content=u'false') 274 field = data_form.Field.fromElement(element) 275 self.assertEquals(u'false', field.value) 276 277 79 278 80 279 class FormTest(unittest.TestCase): 81 280 """ 82 Tests for L{ Form}.281 Tests for L{data_form.Form}. 83 282 """ 84 283 … … 88 287 """ 89 288 90 form = Form('result')289 form = data_form.Form('result') 91 290 self.assertEqual('result', form.formType) 92 291 … … 95 294 The toElement method returns a form's DOM representation. 96 295 """ 97 form = Form('result')296 form = data_form.Form('result') 98 297 element = form.toElement() 99 298 … … 107 306 def test_fromElement(self): 108 307 """ 109 The fromElement static method creates a L{Form} from a L{DOM.308 C{fromElement} creates a L{data_form.Form} from a DOM representation. 110 309 """ 111 310 element = domish.Element((NS_X_DATA, 'x')) 112 311 element['type'] = 'result' 113 form = Form.fromElement(element)312 form = data_form.Form.fromElement(element) 114 313 115 314 self.assertEquals('result', form.formType) … … 124 323 """ 125 324 element = domish.Element((NS_X_DATA, 'form')) 126 self.assertRaises(Exception, Form.fromElement, element)325 self.assertRaises(Exception, data_form.Form.fromElement, element) 127 326 128 327 … … 132 331 """ 133 332 element = domish.Element(('myns', 'x')) 134 self.assertRaises(Exception, Form.fromElement, element)333 self.assertRaises(Exception, data_form.Form.fromElement, element) 135 334 136 335 … … 138 337 element = domish.Element((NS_X_DATA, 'x')) 139 338 element.addElement('title', content='My title') 140 form = Form.fromElement(element)339 form = data_form.Form.fromElement(element) 141 340 142 341 self.assertEquals('My title', form.title) … … 146 345 element = domish.Element((NS_X_DATA, 'x')) 147 346 element.addElement('instructions', content='instruction') 148 form = Form.fromElement(element)347 form = data_form.Form.fromElement(element) 149 348 150 349 self.assertEquals(['instruction'], form.instructions) … … 154 353 element.addElement('instructions', content='instruction 1') 155 354 element.addElement('instructions', content='instruction 2') 156 form = Form.fromElement(element)355 form = data_form.Form.fromElement(element) 157 356 158 357 self.assertEquals(['instruction 1', 'instruction 2'], form.instructions) … … 162 361 element = domish.Element((NS_X_DATA, 'x')) 163 362 element.addElement('field') 164 form = Form.fromElement(element)363 form = data_form.Form.fromElement(element) 165 364 166 365 self.assertEquals(1, len(form.fieldList)) … … 172 371 element.addElement('field')['var'] = 'field1' 173 372 element.addElement('field')['var'] = 'field2' 174 form = Form.fromElement(element)373 form = data_form.Form.fromElement(element) 175 374 176 375 self.assertEquals(2, len(form.fieldList)) -
wokkel/test/test_disco.py
r31 r53 1 # Copyright (c) 2003-200 8Ralph Meijer1 # Copyright (c) 2003-2009 Ralph Meijer 2 2 # See LICENSE for details. 3 3 … … 6 6 """ 7 7 8 from zope.interface import implements 9 8 10 from twisted.internet import defer 9 11 from twisted.trial import unittest 10 from twisted.words.xish.xmlstream import XmlStreamFactory 11 from zope.interface import implements 12 13 from wokkel.subprotocols import XMPPHandler, StreamManager 14 15 from wokkel import disco 12 from twisted.words.protocols.jabber.jid import JID 13 from twisted.words.protocols.jabber.xmlstream import toResponse 14 from twisted.words.xish import domish 15 16 from wokkel import data_form, disco 17 from wokkel.generic import parseXml 18 from wokkel.subprotocols import XMPPHandler 19 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 16 20 17 21 NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info' 18 22 NS_DISCO_ITEMS = 'http://jabber.org/protocol/disco#items' 19 23 20 class DiscoResponder(XMPPHandler): 21 implements(disco.IDisco) 22 23 def getDiscoInfo(self, requestor, target, nodeIdentifier): 24 if not nodeIdentifier: 24 class DiscoFeatureTest(unittest.TestCase): 25 """ 26 Tests for L{disco.DiscoFeature}. 27 """ 28 29 def test_init(self): 30 """ 31 Test initialization with a with feature namespace URI. 32 """ 33 feature = disco.DiscoFeature(u'testns') 34 self.assertEqual(u'testns', feature) 35 36 37 def test_toElement(self): 38 """ 39 Test proper rendering to a DOM representation. 40 41 The returned element should be properly named and have a C{var} 42 attribute that holds the feature namespace URI. 43 """ 44 feature = disco.DiscoFeature(u'testns') 45 element = feature.toElement() 46 self.assertEqual(NS_DISCO_INFO, element.uri) 47 self.assertEqual(u'feature', element.name) 48 self.assertTrue(element.hasAttribute(u'var')) 49 self.assertEqual(u'testns', element[u'var']) 50 51 52 def test_fromElement(self): 53 """ 54 Test creating L{disco.DiscoFeature} from L{domish.Element}. 55 """ 56 element = domish.Element((NS_DISCO_INFO, u'feature')) 57 element['var'] = u'testns' 58 feature = disco.DiscoFeature.fromElement(element) 59 self.assertEqual(u'testns', feature) 60 61 62 63 class DiscoIdentityTest(unittest.TestCase): 64 """ 65 Tests for L{disco.DiscoIdentity}. 66 """ 67 68 def test_init(self): 69 """ 70 Test initialization with a category, type and name. 71 """ 72 identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom') 73 self.assertEqual(u'conference', identity.category) 74 self.assertEqual(u'text', identity.type) 75 self.assertEqual(u'The chatroom', identity.name) 76 77 78 def test_toElement(self): 79 """ 80 Test proper rendering to a DOM representation. 81 82 The returned element should be properly named and have C{conference}, 83 C{type}, and C{name} attributes. 84 """ 85 identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom') 86 element = identity.toElement() 87 self.assertEqual(NS_DISCO_INFO, element.uri) 88 self.assertEqual(u'identity', element.name) 89 self.assertEqual(u'conference', element.getAttribute(u'category')) 90 self.assertEqual(u'text', element.getAttribute(u'type')) 91 self.assertEqual(u'The chatroom', element.getAttribute(u'name')) 92 93 94 def test_toElementWithoutName(self): 95 """ 96 Test proper rendering to a DOM representation without a name. 97 98 The returned element should be properly named and have C{conference}, 99 C{type} attributes, no C{name} attribute. 100 """ 101 identity = disco.DiscoIdentity(u'conference', u'text') 102 element = identity.toElement() 103 self.assertEqual(NS_DISCO_INFO, element.uri) 104 self.assertEqual(u'identity', element.name) 105 self.assertEqual(u'conference', element.getAttribute(u'category')) 106 self.assertEqual(u'text', element.getAttribute(u'type')) 107 self.assertFalse(element.hasAttribute(u'name')) 108 109 110 def test_fromElement(self): 111 """ 112 Test creating L{disco.DiscoIdentity} from L{domish.Element}. 113 """ 114 element = domish.Element((NS_DISCO_INFO, u'identity')) 115 element['category'] = u'conference' 116 element['type'] = u'text' 117 element['name'] = u'The chatroom' 118 identity = disco.DiscoIdentity.fromElement(element) 119 self.assertEqual(u'conference', identity.category) 120 self.assertEqual(u'text', identity.type) 121 self.assertEqual(u'The chatroom', identity.name) 122 123 124 def test_fromElementWithoutName(self): 125 """ 126 Test creating L{disco.DiscoIdentity} from L{domish.Element}, no name. 127 """ 128 element = domish.Element((NS_DISCO_INFO, u'identity')) 129 element['category'] = u'conference' 130 element['type'] = u'text' 131 identity = disco.DiscoIdentity.fromElement(element) 132 self.assertEqual(u'conference', identity.category) 133 self.assertEqual(u'text', identity.type) 134 self.assertEqual(None, identity.name) 135 136 137 138 class DiscoInfoTest(unittest.TestCase): 139 """ 140 Tests for L{disco.DiscoInfo}. 141 """ 142 143 def test_toElement(self): 144 """ 145 Test C{toElement} creates a correctly namespaced element, no node. 146 """ 147 info = disco.DiscoInfo() 148 element = info.toElement() 149 150 self.assertEqual(NS_DISCO_INFO, element.uri) 151 self.assertEqual(u'query', element.name) 152 self.assertFalse(element.hasAttribute(u'node')) 153 154 155 def test_toElementNode(self): 156 """ 157 Test C{toElement} with a node. 158 """ 159 info = disco.DiscoInfo() 160 info.nodeIdentifier = u'test' 161 element = info.toElement() 162 163 self.assertEqual(u'test', element.getAttribute(u'node')) 164 165 166 def test_toElementChildren(self): 167 """ 168 Test C{toElement} creates a DOM with proper childs. 169 """ 170 info = disco.DiscoInfo() 171 info.append(disco.DiscoFeature(u'jabber:iq:register')) 172 info.append(disco.DiscoIdentity(u'conference', u'text')) 173 info.append(data_form.Form(u'result')) 174 element = info.toElement() 175 176 featureElements = domish.generateElementsQNamed(element.children, 177 u'feature', 178 NS_DISCO_INFO) 179 self.assertEqual(1, len(list(featureElements))) 180 181 identityElements = domish.generateElementsQNamed(element.children, 182 u'identity', 183 NS_DISCO_INFO) 184 self.assertEqual(1, len(list(identityElements))) 185 186 extensionElements = domish.generateElementsQNamed(element.children, 187 u'x', 188 data_form.NS_X_DATA) 189 self.assertEqual(1, len(list(extensionElements))) 190 191 192 def test_fromElement(self): 193 """ 194 Test properties when creating L{disco.DiscoInfo} from L{domish.Element}. 195 """ 196 xml = """<query xmlns='http://jabber.org/protocol/disco#info'> 197 <identity category='conference' 198 type='text' 199 name='A Dark Cave'/> 200 <feature var='http://jabber.org/protocol/muc'/> 201 <feature var='jabber:iq:register'/> 202 <x xmlns='jabber:x:data' type='result'> 203 <field var='FORM_TYPE' type='hidden'> 204 <value>http://jabber.org/protocol/muc#roominfo</value> 205 </field> 206 </x> 207 </query>""" 208 209 element = parseXml(xml) 210 info = disco.DiscoInfo.fromElement(element) 211 212 self.assertIn(u'http://jabber.org/protocol/muc', info.features) 213 self.assertIn(u'jabber:iq:register', info.features) 214 215 self.assertIn((u'conference', u'text'), info.identities) 216 self.assertEqual(u'A Dark Cave', 217 info.identities[(u'conference', u'text')]) 218 219 self.assertIn(u'http://jabber.org/protocol/muc#roominfo', 220 info.extensions) 221 222 223 def test_fromElementItems(self): 224 """ 225 Test items when creating L{disco.DiscoInfo} from L{domish.Element}. 226 """ 227 xml = """<query xmlns='http://jabber.org/protocol/disco#info'> 228 <identity category='conference' 229 type='text' 230 name='A Dark Cave'/> 231 <feature var='http://jabber.org/protocol/muc'/> 232 <feature var='jabber:iq:register'/> 233 <x xmlns='jabber:x:data' type='result'> 234 <field var='FORM_TYPE' type='hidden'> 235 <value>http://jabber.org/protocol/muc#roominfo</value> 236 </field> 237 </x> 238 </query>""" 239 240 element = parseXml(xml) 241 info = disco.DiscoInfo.fromElement(element) 242 243 info = list(info) 244 self.assertEqual(4, len(info)) 245 246 identity = info[0] 247 self.assertEqual(u'conference', identity.category) 248 249 self.assertEqual(u'http://jabber.org/protocol/muc', info[1]) 250 self.assertEqual(u'jabber:iq:register', info[2]) 251 252 extension = info[3] 253 self.assertEqual(u'http://jabber.org/protocol/muc#roominfo', 254 extension.formNamespace) 255 256 257 def test_fromElementNoNode(self): 258 """ 259 Test creating L{disco.DiscoInfo} from L{domish.Element}, no node. 260 """ 261 xml = """<query xmlns='http://jabber.org/protocol/disco#info'/>""" 262 263 element = parseXml(xml) 264 info = disco.DiscoInfo.fromElement(element) 265 266 self.assertEqual(u'', info.nodeIdentifier) 267 268 269 def test_fromElementNode(self): 270 """ 271 Test creating L{disco.DiscoInfo} from L{domish.Element}, with node. 272 """ 273 xml = """<query xmlns='http://jabber.org/protocol/disco#info' 274 node='test'> 275 </query>""" 276 277 element = parseXml(xml) 278 info = disco.DiscoInfo.fromElement(element) 279 280 self.assertEqual(u'test', info.nodeIdentifier) 281 282 283 284 class DiscoItemTest(unittest.TestCase): 285 """ 286 Tests for L{disco.DiscoItem}. 287 """ 288 289 def test_init(self): 290 """ 291 Test initialization with a category, type and name. 292 """ 293 item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node') 294 self.assertEqual(JID(u'example.org'), item.entity) 295 self.assertEqual(u'test', item.nodeIdentifier) 296 self.assertEqual(u'The node', item.name) 297 298 299 def test_toElement(self): 300 """ 301 Test proper rendering to a DOM representation. 302 303 The returned element should be properly named and have C{jid}, C{node}, 304 and C{name} attributes. 305 """ 306 item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node') 307 element = item.toElement() 308 self.assertEqual(NS_DISCO_ITEMS, element.uri) 309 self.assertEqual(u'item', element.name) 310 self.assertEqual(u'example.org', element.getAttribute(u'jid')) 311 self.assertEqual(u'test', element.getAttribute(u'node')) 312 self.assertEqual(u'The node', element.getAttribute(u'name')) 313 314 315 def test_toElementWithoutName(self): 316 """ 317 Test proper rendering to a DOM representation without a name. 318 319 The returned element should be properly named and have C{jid}, C{node} 320 attributes, no C{name} attribute. 321 """ 322 item = disco.DiscoItem(JID(u'example.org'), u'test') 323 element = item.toElement() 324 self.assertEqual(NS_DISCO_ITEMS, element.uri) 325 self.assertEqual(u'item', element.name) 326 self.assertEqual(u'example.org', element.getAttribute(u'jid')) 327 self.assertEqual(u'test', element.getAttribute(u'node')) 328 self.assertFalse(element.hasAttribute(u'name')) 329 330 331 def test_fromElement(self): 332 """ 333 Test creating L{disco.DiscoItem} from L{domish.Element}. 334 """ 335 element = domish.Element((NS_DISCO_ITEMS, u'item')) 336 element[u'jid'] = u'example.org' 337 element[u'node'] = u'test' 338 element[u'name'] = u'The node' 339 item = disco.DiscoItem.fromElement(element) 340 self.assertEqual(JID(u'example.org'), item.entity) 341 self.assertEqual(u'test', item.nodeIdentifier) 342 self.assertEqual(u'The node', item.name) 343 344 def test_fromElementNoNode(self): 345 """ 346 Test creating L{disco.DiscoItem} from L{domish.Element}, no node. 347 """ 348 element = domish.Element((NS_DISCO_ITEMS, u'item')) 349 element[u'jid'] = u'example.org' 350 element[u'name'] = u'The node' 351 item = disco.DiscoItem.fromElement(element) 352 self.assertEqual(JID(u'example.org'), item.entity) 353 self.assertEqual(u'', item.nodeIdentifier) 354 self.assertEqual(u'The node', item.name) 355 356 357 def test_fromElementNoName(self): 358 """ 359 Test creating L{disco.DiscoItem} from L{domish.Element}, no name. 360 """ 361 element = domish.Element((NS_DISCO_ITEMS, u'item')) 362 element[u'jid'] = u'example.org' 363 element[u'node'] = u'test' 364 item = disco.DiscoItem.fromElement(element) 365 self.assertEqual(JID(u'example.org'), item.entity) 366 self.assertEqual(u'test', item.nodeIdentifier) 367 self.assertEqual(None, item.name) 368 369 def test_fromElementBadJID(self): 370 """ 371 Test creating L{disco.DiscoItem} from L{domish.Element}, bad JID. 372 """ 373 element = domish.Element((NS_DISCO_ITEMS, u'item')) 374 element[u'jid'] = u'ex@@@ample.org' 375 item = disco.DiscoItem.fromElement(element) 376 self.assertIdentical(None, item.entity) 377 378 379 380 class DiscoItemsTest(unittest.TestCase): 381 """ 382 Tests for L{disco.DiscoItems}. 383 """ 384 385 def test_toElement(self): 386 """ 387 Test C{toElement} creates a correctly namespaced element, no node. 388 """ 389 items = disco.DiscoItems() 390 element = items.toElement() 391 392 self.assertEqual(NS_DISCO_ITEMS, element.uri) 393 self.assertEqual(u'query', element.name) 394 self.assertFalse(element.hasAttribute(u'node')) 395 396 397 def test_toElementNode(self): 398 """ 399 Test C{toElement} with a node. 400 """ 401 items = disco.DiscoItems() 402 items.nodeIdentifier = u'test' 403 element = items.toElement() 404 405 self.assertEqual(u'test', element.getAttribute(u'node')) 406 407 408 def test_toElementChildren(self): 409 """ 410 Test C{toElement} creates a DOM with proper childs. 411 """ 412 items = disco.DiscoItems() 413 items.append(disco.DiscoItem(JID(u'example.org'), u'test', u'A node')) 414 element = items.toElement() 415 416 itemElements = domish.generateElementsQNamed(element.children, 417 u'item', 418 NS_DISCO_ITEMS) 419 self.assertEqual(1, len(list(itemElements))) 420 421 422 def test_fromElement(self): 423 """ 424 Test creating L{disco.DiscoItems} from L{domish.Element}. 425 """ 426 xml = """<query xmlns='http://jabber.org/protocol/disco#items'> 427 <item jid='example.org' node='test' name='A node'/> 428 </query>""" 429 430 element = parseXml(xml) 431 items = disco.DiscoItems.fromElement(element) 432 433 items = list(items) 434 self.assertEqual(1, len(items)) 435 item = items[0] 436 437 self.assertEqual(JID(u'example.org'), item.entity) 438 self.assertEqual(u'test', item.nodeIdentifier) 439 self.assertEqual(u'A node', item.name) 440 441 442 def test_fromElementNoNode(self): 443 """ 444 Test creating L{disco.DiscoItems} from L{domish.Element}, no node. 445 """ 446 xml = """<query xmlns='http://jabber.org/protocol/disco#items'/>""" 447 448 element = parseXml(xml) 449 items = disco.DiscoItems.fromElement(element) 450 451 self.assertEqual(u'', items.nodeIdentifier) 452 453 454 def test_fromElementNode(self): 455 """ 456 Test creating L{disco.DiscoItems} from L{domish.Element}, with node. 457 """ 458 xml = """<query xmlns='http://jabber.org/protocol/disco#items' 459 node='test'> 460 </query>""" 461 462 element = parseXml(xml) 463 items = disco.DiscoItems.fromElement(element) 464 465 self.assertEqual(u'test', items.nodeIdentifier) 466 467 468 469 class DiscoClientProtocolTest(unittest.TestCase): 470 """ 471 Tests for L{disco.DiscoClientProtocol}. 472 """ 473 474 def setUp(self): 475 """ 476 Set up stub and protocol for testing. 477 """ 478 self.stub = XmlStreamStub() 479 self.protocol = disco.DiscoClientProtocol() 480 self.protocol.xmlstream = self.stub.xmlstream 481 self.protocol.connectionInitialized() 482 483 484 def test_requestItems(self): 485 """ 486 Test request sent out by C{requestItems} and parsing of response. 487 """ 488 def cb(items): 489 items = list(items) 490 self.assertEqual(2, len(items)) 491 self.assertEqual(JID(u'test.example.org'), items[0].entity) 492 493 d = self.protocol.requestItems(JID(u'example.org'),u"foo") 494 d.addCallback(cb) 495 496 iq = self.stub.output[-1] 497 self.assertEqual(u'example.org', iq.getAttribute(u'to')) 498 self.assertEqual(u'get', iq.getAttribute(u'type')) 499 self.assertEqual(u'foo', iq.query.getAttribute(u'node')) 500 self.assertEqual(NS_DISCO_ITEMS, iq.query.uri) 501 502 response = toResponse(iq, u'result') 503 query = response.addElement((NS_DISCO_ITEMS, u'query')) 504 505 element = query.addElement(u'item') 506 element[u'jid'] = u'test.example.org' 507 element[u'node'] = u'music' 508 element[u'name'] = u'Music from the time of Shakespeare' 509 510 element = query.addElement(u'item') 511 element[u'jid'] = u"test2.example.org" 512 513 self.stub.send(response) 514 return d 515 516 517 def test_requestInfo(self): 518 """ 519 Test request sent out by C{requestInfo} and parsing of response. 520 """ 521 def cb(info): 522 self.assertIn((u'conference', u'text'), info.identities) 523 self.assertIn(u'http://jabber.org/protocol/disco#info', 524 info.features) 525 self.assertIn(u'http://jabber.org/protocol/muc', 526 info.features) 527 528 d = self.protocol.requestInfo(JID(u'example.org'),'foo') 529 d.addCallback(cb) 530 531 iq = self.stub.output[-1] 532 self.assertEqual(u'example.org', iq.getAttribute(u'to')) 533 self.assertEqual(u'get', iq.getAttribute(u'type')) 534 self.assertEqual(u'foo', iq.query.getAttribute(u'node')) 535 self.assertEqual(NS_DISCO_INFO, iq.query.uri) 536 537 response = toResponse(iq, u'result') 538 query = response.addElement((NS_DISCO_INFO, u'query')) 539 540 element = query.addElement(u"identity") 541 element[u'category'] = u'conference' # required 542 element[u'type'] = u'text' # required 543 element[u"name"] = u'Romeo and Juliet, Act II, Scene II' # optional 544 545 element = query.addElement("feature") 546 element[u'var'] = u'http://jabber.org/protocol/disco#info' # required 547 548 element = query.addElement(u"feature") 549 element[u'var'] = u'http://jabber.org/protocol/muc' 550 551 self.stub.send(response) 552 return d 553 554 555 556 class DiscoHandlerTest(unittest.TestCase, TestableRequestHandlerMixin): 557 """ 558 Tests for L{disco.DiscoHandler}. 559 """ 560 561 def setUp(self): 562 self.service = disco.DiscoHandler() 563 564 565 def test_onDiscoInfo(self): 566 """ 567 C{onDiscoInfo} should process an info request and return a response. 568 569 The request should be parsed, C{info} called with the extracted 570 parameters, and then the result should be formatted into a proper 571 response element. 572 """ 573 xml = """<iq from='test@example.com' to='example.com' 574 type='get'> 575 <query xmlns='%s'/> 576 </iq>""" % NS_DISCO_INFO 577 578 def cb(element): 579 self.assertEqual('query', element.name) 580 self.assertEqual(NS_DISCO_INFO, element.uri) 581 self.assertEqual(NS_DISCO_INFO, element.identity.uri) 582 self.assertEqual('dummy', element.identity['category']) 583 self.assertEqual('generic', element.identity['type']) 584 self.assertEqual('Generic Dummy Entity', element.identity['name']) 585 self.assertEqual(NS_DISCO_INFO, element.feature.uri) 586 self.assertEqual('jabber:iq:version', element.feature['var']) 587 588 def info(requestor, target, nodeIdentifier): 589 self.assertEqual(JID('test@example.com'), requestor) 590 self.assertEqual(JID('example.com'), target) 591 self.assertEqual('', nodeIdentifier) 592 25 593 return defer.succeed([ 26 594 disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'), 27 595 disco.DiscoFeature('jabber:iq:version') 28 596 ]) 29 else: 30 return defer.succeed([]) 31 32 class DiscoHandlerTest(unittest.TestCase): 33 def test_DiscoInfo(self): 34 factory = XmlStreamFactory() 35 sm = StreamManager(factory) 36 disco.DiscoHandler().setHandlerParent(sm) 37 DiscoResponder().setHandlerParent(sm) 38 xs = factory.buildProtocol(None) 39 output = [] 40 xs.send = output.append 41 xs.connectionMade() 42 xs.dispatch(xs, "//event/stream/authd") 43 xs.dataReceived("<stream>") 44 xs.dataReceived("""<iq from='test@example.com' to='example.com' 45 type='get'> 46 <query xmlns='%s'/> 47 </iq>""" % NS_DISCO_INFO) 48 reply = output[0] 49 self.assertEqual(NS_DISCO_INFO, reply.query.uri) 50 self.assertEqual(NS_DISCO_INFO, reply.query.identity.uri) 51 self.assertEqual('dummy', reply.query.identity['category']) 52 self.assertEqual('generic', reply.query.identity['type']) 53 self.assertEqual('Generic Dummy Entity', reply.query.identity['name']) 54 self.assertEqual(NS_DISCO_INFO, reply.query.feature.uri) 55 self.assertEqual('jabber:iq:version', reply.query.feature['var']) 56 597 598 self.service.info = info 599 d = self.handleRequest(xml) 600 d.addCallback(cb) 601 return d 602 603 604 def test_onDiscoInfoWithNode(self): 605 """ 606 An info request for a node should return it in the response. 607 """ 608 xml = """<iq from='test@example.com' to='example.com' 609 type='get'> 610 <query xmlns='%s' node='test'/> 611 </iq>""" % NS_DISCO_INFO 612 613 def cb(element): 614 self.assertTrue(element.hasAttribute('node')) 615 self.assertEqual('test', element['node']) 616 617 def info(requestor, target, nodeIdentifier): 618 self.assertEqual('test', nodeIdentifier) 619 620 return defer.succeed([ 621 disco.DiscoFeature('jabber:iq:version') 622 ]) 623 624 self.service.info = info 625 d = self.handleRequest(xml) 626 d.addCallback(cb) 627 return d 628 629 630 def test_onDiscoItems(self): 631 """ 632 C{onDiscoItems} should process an items request and return a response. 633 634 The request should be parsed, C{items} called with the extracted 635 parameters, and then the result should be formatted into a proper 636 response element. 637 """ 638 xml = """<iq from='test@example.com' to='example.com' 639 type='get'> 640 <query xmlns='%s'/> 641 </iq>""" % NS_DISCO_ITEMS 642 643 def cb(element): 644 self.assertEqual('query', element.name) 645 self.assertEqual(NS_DISCO_ITEMS, element.uri) 646 self.assertEqual(NS_DISCO_ITEMS, element.item.uri) 647 self.assertEqual('example.com', element.item['jid']) 648 self.assertEqual('test', element.item['node']) 649 self.assertEqual('Test node', element.item['name']) 650 651 def items(requestor, target, nodeIdentifier): 652 self.assertEqual(JID('test@example.com'), requestor) 653 self.assertEqual(JID('example.com'), target) 654 self.assertEqual('', nodeIdentifier) 655 656 return defer.succeed([ 657 disco.DiscoItem(JID('example.com'), 'test', 'Test node'), 658 ]) 659 660 self.service.items = items 661 d = self.handleRequest(xml) 662 d.addCallback(cb) 663 return d 664 665 666 def test_onDiscoItemsWithNode(self): 667 """ 668 An items request for a node should return it in the response. 669 """ 670 xml = """<iq from='test@example.com' to='example.com' 671 type='get'> 672 <query xmlns='%s' node='test'/> 673 </iq>""" % NS_DISCO_ITEMS 674 675 def cb(element): 676 self.assertTrue(element.hasAttribute('node')) 677 self.assertEqual('test', element['node']) 678 679 def items(requestor, target, nodeIdentifier): 680 self.assertEqual('test', nodeIdentifier) 681 682 return defer.succeed([ 683 disco.DiscoFeature('jabber:iq:version') 684 ]) 685 686 self.service.items = items 687 d = self.handleRequest(xml) 688 d.addCallback(cb) 689 return d 690 691 692 def test_info(self): 693 """ 694 C{info} should gather disco info from sibling handlers. 695 """ 696 discoItems = [disco.DiscoIdentity('dummy', 'generic', 697 'Generic Dummy Entity'), 698 disco.DiscoFeature('jabber:iq:version') 699 ] 700 701 class DiscoResponder(XMPPHandler): 702 implements(disco.IDisco) 703 704 def getDiscoInfo(self, requestor, target, nodeIdentifier): 705 if not nodeIdentifier: 706 return defer.succeed(discoItems) 707 else: 708 return defer.succeed([]) 709 710 def cb(result): 711 self.assertEquals(discoItems, result) 712 713 self.service.parent = [self.service, DiscoResponder()] 714 d = self.service.info(JID('test@example.com'), JID('example.com'), '') 715 d.addCallback(cb) 716 return d 717 718 719 def test_items(self): 720 """ 721 C{info} should gather disco items from sibling handlers. 722 """ 723 discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')] 724 725 class DiscoResponder(XMPPHandler): 726 implements(disco.IDisco) 727 728 def getDiscoItems(self, requestor, target, nodeIdentifier): 729 if not nodeIdentifier: 730 return defer.succeed(discoItems) 731 else: 732 return defer.succeed([]) 733 734 def cb(result): 735 self.assertEquals(discoItems, result) 736 737 self.service.parent = [self.service, DiscoResponder()] 738 d = self.service.items(JID('test@example.com'), JID('example.com'), '') 739 d.addCallback(cb) 740 return d -
wokkel/test/test_generic.py
r20 r35 49 49 self.assertEquals(1, len(elements)) 50 50 self.assertEquals('0.1.0', unicode(elements[0])) 51 52 53 54 class XmlPipeTest(unittest.TestCase): 55 """ 56 Tests for L{wokkel.generic.XmlPipe}. 57 """ 58 59 def setUp(self): 60 self.pipe = generic.XmlPipe() 61 62 63 def test_sendFromSource(self): 64 """ 65 Send an element from the source and observe it from the sink. 66 """ 67 def cb(obj): 68 called.append(obj) 69 70 called = [] 71 self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb) 72 element = domish.Element(('testns', 'test')) 73 self.pipe.source.send(element) 74 self.assertEquals([element], called) 75 76 77 def test_sendFromSink(self): 78 """ 79 Send an element from the sink and observe it from the source. 80 """ 81 def cb(obj): 82 called.append(obj) 83 84 called = [] 85 self.pipe.source.addObserver('/test[@xmlns="testns"]', cb) 86 element = domish.Element(('testns', 'test')) 87 self.pipe.sink.send(element) 88 self.assertEquals([element], called) -
wokkel/test/test_pubsub.py
r30 r59 1 # Copyright (c) 2003-200 8Ralph Meijer1 # Copyright (c) 2003-2009 Ralph Meijer 2 2 # See LICENSE for details. 3 3 … … 10 10 from twisted.trial import unittest 11 11 from twisted.internet import defer 12 from twisted.words.xish import domish , xpath12 from twisted.words.xish import domish 13 13 from twisted.words.protocols.jabber import error 14 14 from twisted.words.protocols.jabber.jid import JID 15 16 from wokkel import data_form, iwokkel, pubsub, shim 15 from twisted.words.protocols.jabber.xmlstream import toResponse 16 17 from wokkel import data_form, disco, iwokkel, pubsub, shim 17 18 from wokkel.generic import parseXml 18 from wokkel.test.helpers import XmlStreamStub 19 20 try: 21 from twisted.words.protocols.jabber.xmlstream import toResponse 22 except ImportError: 23 from wokkel.compat import toResponse 19 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 24 20 25 21 NS_PUBSUB = 'http://jabber.org/protocol/pubsub' … … 28 24 NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' 29 25 NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner' 26 NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data' 30 27 31 28 def calledAsync(fn): … … 116 113 117 114 118 def test_event _delete(self):115 def test_eventDelete(self): 119 116 """ 120 117 Test receiving a delete event resulting in a call to deleteReceived. … … 124 121 message['to'] = 'user@example.org/home' 125 122 event = message.addElement((NS_PUBSUB_EVENT, 'event')) 126 items= event.addElement('delete')127 items['node'] = 'test'123 delete = event.addElement('delete') 124 delete['node'] = 'test' 128 125 129 126 def deleteReceived(event): … … 131 128 self.assertEquals(JID('pubsub.example.org'), event.sender) 132 129 self.assertEquals('test', event.nodeIdentifier) 130 131 d, self.protocol.deleteReceived = calledAsync(deleteReceived) 132 self.stub.send(message) 133 return d 134 135 136 def test_eventDeleteRedirect(self): 137 """ 138 Test receiving a delete event with a redirect URI. 139 """ 140 message = domish.Element((None, 'message')) 141 message['from'] = 'pubsub.example.org' 142 message['to'] = 'user@example.org/home' 143 event = message.addElement((NS_PUBSUB_EVENT, 'event')) 144 delete = event.addElement('delete') 145 delete['node'] = 'test' 146 uri = 'xmpp:pubsub.example.org?;node=test2' 147 delete.addElement('redirect')['uri'] = uri 148 149 def deleteReceived(event): 150 self.assertEquals(JID('user@example.org/home'), event.recipient) 151 self.assertEquals(JID('pubsub.example.org'), event.sender) 152 self.assertEquals('test', event.nodeIdentifier) 153 self.assertEquals(uri, event.redirectURI) 133 154 134 155 d, self.protocol.deleteReceived = calledAsync(deleteReceived) … … 235 256 236 257 258 def test_createNodeWithSender(self): 259 """ 260 Test sending create request from a specific JID. 261 """ 262 263 d = self.protocol.createNode(JID('pubsub.example.org'), 'test', 264 sender=JID('user@example.org')) 265 266 iq = self.stub.output[-1] 267 self.assertEquals('user@example.org', iq['from']) 268 269 response = toResponse(iq, 'result') 270 self.stub.send(response) 271 return d 272 273 237 274 def test_deleteNode(self): 238 275 """ … … 246 283 self.assertEquals('set', iq.getAttribute('type')) 247 284 self.assertEquals('pubsub', iq.pubsub.name) 248 self.assertEquals(NS_PUBSUB , iq.pubsub.uri)285 self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri) 249 286 children = list(domish.generateElementsQNamed(iq.pubsub.children, 250 'delete', NS_PUBSUB ))287 'delete', NS_PUBSUB_OWNER)) 251 288 self.assertEquals(1, len(children)) 252 289 child = children[0] 253 290 self.assertEquals('test', child['node']) 291 292 response = toResponse(iq, 'result') 293 self.stub.send(response) 294 return d 295 296 297 def test_deleteNodeWithSender(self): 298 """ 299 Test sending delete request. 300 """ 301 302 d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test', 303 sender=JID('user@example.org')) 304 305 iq = self.stub.output[-1] 306 self.assertEquals('user@example.org', iq['from']) 254 307 255 308 response = toResponse(iq, 'result') … … 309 362 310 363 364 def test_publishWithSender(self): 365 """ 366 Test sending publish request from a specific JID. 367 """ 368 369 item = pubsub.Item() 370 d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item], 371 JID('user@example.org')) 372 373 iq = self.stub.output[-1] 374 self.assertEquals('user@example.org', iq['from']) 375 376 response = toResponse(iq, 'result') 377 self.stub.send(response) 378 return d 379 380 311 381 def test_subscribe(self): 312 382 """ … … 378 448 379 449 450 def test_subscribeWithSender(self): 451 """ 452 Test sending subscription request from a specific JID. 453 """ 454 d = self.protocol.subscribe(JID('pubsub.example.org'), 'test', 455 JID('user@example.org'), 456 sender=JID('user@example.org')) 457 458 iq = self.stub.output[-1] 459 self.assertEquals('user@example.org', iq['from']) 460 461 response = toResponse(iq, 'result') 462 pubsub = response.addElement((NS_PUBSUB, 'pubsub')) 463 subscription = pubsub.addElement('subscription') 464 subscription['node'] = 'test' 465 subscription['jid'] = 'user@example.org' 466 subscription['subscription'] = 'subscribed' 467 self.stub.send(response) 468 return d 469 470 380 471 def test_unsubscribe(self): 381 472 """ … … 401 492 402 493 494 def test_unsubscribeWithSender(self): 495 """ 496 Test sending unsubscription request from a specific JID. 497 """ 498 d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test', 499 JID('user@example.org'), 500 sender=JID('user@example.org')) 501 502 iq = self.stub.output[-1] 503 self.assertEquals('user@example.org', iq['from']) 504 self.stub.send(toResponse(iq, 'result')) 505 return d 506 507 403 508 def test_items(self): 404 509 """ … … 467 572 468 573 469 470 class PubSubServiceTest(unittest.TestCase): 471 """ 472 Tests for L{pubsub.PubSubService}. 473 """ 474 475 def setUp(self): 476 self.service = pubsub.PubSubService() 477 478 def handleRequest(self, xml): 479 """ 480 Find a handler and call it directly 481 """ 482 handler = None 483 iq = parseXml(xml) 484 for queryString, method in self.service.iqHandlers.iteritems(): 485 if xpath.internQuery(queryString).matches(iq): 486 handler = getattr(self.service, method) 487 488 if handler: 489 d = defer.maybeDeferred(handler, iq) 490 else: 491 d = defer.fail(NotImplementedError()) 492 493 return d 494 495 496 def test_interface(self): 497 """ 498 Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}? 499 """ 500 verify.verifyObject(iwokkel.IPubSubService, self.service) 501 502 503 def test_onPublishNoNode(self): 504 """ 505 The root node is always a collection, publishing is a bad request. 574 def test_itemsWithSender(self): 575 """ 576 Test sending items request from a specific JID. 577 """ 578 579 d = self.protocol.items(JID('pubsub.example.org'), 'test', 580 sender=JID('user@example.org')) 581 582 iq = self.stub.output[-1] 583 self.assertEquals('user@example.org', iq['from']) 584 585 response = toResponse(iq, 'result') 586 items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items') 587 items['node'] = 'test' 588 589 self.stub.send(response) 590 return d 591 592 593 594 class PubSubRequestTest(unittest.TestCase): 595 596 def test_fromElementPublish(self): 597 """ 598 Test parsing a publish request. 599 """ 600 601 xml = """ 602 <iq type='set' to='pubsub.example.org' 603 from='user@example.org'> 604 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 605 <publish node='test'/> 606 </pubsub> 607 </iq> 608 """ 609 610 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 611 self.assertEqual('publish', request.verb) 612 self.assertEqual(JID('user@example.org'), request.sender) 613 self.assertEqual(JID('pubsub.example.org'), request.recipient) 614 self.assertEqual('test', request.nodeIdentifier) 615 self.assertEqual([], request.items) 616 617 618 def test_fromElementPublishItems(self): 619 """ 620 Test parsing a publish request with items. 621 """ 622 623 xml = """ 624 <iq type='set' to='pubsub.example.org' 625 from='user@example.org'> 626 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 627 <publish node='test'> 628 <item id="item1"/> 629 <item id="item2"/> 630 </publish> 631 </pubsub> 632 </iq> 633 """ 634 635 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 636 self.assertEqual(2, len(request.items)) 637 self.assertEqual(u'item1', request.items[0]["id"]) 638 self.assertEqual(u'item2', request.items[1]["id"]) 639 640 641 def test_fromElementPublishNoNode(self): 642 """ 643 A publish request to the root node should raise an exception. 506 644 """ 507 645 xml = """ … … 514 652 """ 515 653 516 def cb(result): 517 self.assertEquals('bad-request', result.condition) 518 519 d = self.handleRequest(xml) 520 self.assertFailure(d, error.StanzaError) 521 d.addCallback(cb) 522 return d 523 524 525 def test_onPublish(self): 526 """ 527 A publish request should result in L{PubSubService.publish} being 528 called. 529 """ 530 531 xml = """ 532 <iq type='set' to='pubsub.example.org' 533 from='user@example.org'> 534 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 535 <publish node='test'/> 536 </pubsub> 537 </iq> 538 """ 539 540 def publish(requestor, service, nodeIdentifier, items): 541 self.assertEqual(JID('user@example.org'), requestor) 542 self.assertEqual(JID('pubsub.example.org'), service) 543 self.assertEqual('test', nodeIdentifier) 544 self.assertEqual([], items) 545 return defer.succeed(None) 546 547 self.service.publish = publish 548 return self.handleRequest(xml) 549 550 551 def test_onOptionsGet(self): 552 """ 553 Subscription options are not supported. 654 err = self.assertRaises(error.StanzaError, 655 pubsub.PubSubRequest.fromElement, 656 parseXml(xml)) 657 self.assertEqual('bad-request', err.condition) 658 self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) 659 self.assertEqual('nodeid-required', err.appCondition.name) 660 661 662 def test_fromElementSubscribe(self): 663 """ 664 Test parsing a subscription request. 665 """ 666 667 xml = """ 668 <iq type='set' to='pubsub.example.org' 669 from='user@example.org'> 670 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 671 <subscribe node='test' jid='user@example.org/Home'/> 672 </pubsub> 673 </iq> 674 """ 675 676 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 677 self.assertEqual('subscribe', request.verb) 678 self.assertEqual(JID('user@example.org'), request.sender) 679 self.assertEqual(JID('pubsub.example.org'), request.recipient) 680 self.assertEqual('test', request.nodeIdentifier) 681 self.assertEqual(JID('user@example.org/Home'), request.subscriber) 682 683 684 def test_fromElementSubscribeEmptyNode(self): 685 """ 686 Test parsing a subscription request to the root node. 687 """ 688 689 xml = """ 690 <iq type='set' to='pubsub.example.org' 691 from='user@example.org'> 692 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 693 <subscribe jid='user@example.org/Home'/> 694 </pubsub> 695 </iq> 696 """ 697 698 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 699 self.assertEqual('', request.nodeIdentifier) 700 701 702 def test_fromElementSubscribeNoJID(self): 703 """ 704 Subscribe requests without a JID should raise a bad-request exception. 705 """ 706 xml = """ 707 <iq type='set' to='pubsub.example.org' 708 from='user@example.org'> 709 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 710 <subscribe node='test'/> 711 </pubsub> 712 </iq> 713 """ 714 err = self.assertRaises(error.StanzaError, 715 pubsub.PubSubRequest.fromElement, 716 parseXml(xml)) 717 self.assertEqual('bad-request', err.condition) 718 self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) 719 self.assertEqual('jid-required', err.appCondition.name) 720 721 def test_fromElementUnsubscribe(self): 722 """ 723 Test parsing an unsubscription request. 724 """ 725 726 xml = """ 727 <iq type='set' to='pubsub.example.org' 728 from='user@example.org'> 729 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 730 <unsubscribe node='test' jid='user@example.org/Home'/> 731 </pubsub> 732 </iq> 733 """ 734 735 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 736 self.assertEqual('unsubscribe', request.verb) 737 self.assertEqual(JID('user@example.org'), request.sender) 738 self.assertEqual(JID('pubsub.example.org'), request.recipient) 739 self.assertEqual('test', request.nodeIdentifier) 740 self.assertEqual(JID('user@example.org/Home'), request.subscriber) 741 742 743 def test_fromElementUnsubscribeNoJID(self): 744 """ 745 Unsubscribe requests without a JID should raise a bad-request exception. 746 """ 747 xml = """ 748 <iq type='set' to='pubsub.example.org' 749 from='user@example.org'> 750 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 751 <unsubscribe node='test'/> 752 </pubsub> 753 </iq> 754 """ 755 err = self.assertRaises(error.StanzaError, 756 pubsub.PubSubRequest.fromElement, 757 parseXml(xml)) 758 self.assertEqual('bad-request', err.condition) 759 self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri) 760 self.assertEqual('jid-required', err.appCondition.name) 761 762 763 def test_fromElementOptionsGet(self): 764 """ 765 Test parsing a request for getting subscription options. 554 766 """ 555 767 … … 558 770 from='user@example.org'> 559 771 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 560 <options/> 561 </pubsub> 562 </iq> 563 """ 564 565 def cb(result): 566 self.assertEquals('feature-not-implemented', result.condition) 567 self.assertEquals('unsupported', result.appCondition.name) 568 self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri) 569 570 d = self.handleRequest(xml) 571 self.assertFailure(d, error.StanzaError) 572 d.addCallback(cb) 573 return d 574 575 576 def test_onDefault(self): 577 """ 578 A default request should result in 579 L{PubSubService.getDefaultConfiguration} being called. 772 <options node='test' jid='user@example.org/Home'/> 773 </pubsub> 774 </iq> 775 """ 776 777 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 778 self.assertEqual('optionsGet', request.verb) 779 780 781 def test_fromElementOptionsSet(self): 782 """ 783 Test parsing a request for setting subscription options. 784 """ 785 786 xml = """ 787 <iq type='set' to='pubsub.example.org' 788 from='user@example.org'> 789 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 790 <options node='test' jid='user@example.org/Home'> 791 <x xmlns='jabber:x:data' type='submit'> 792 <field var='FORM_TYPE' type='hidden'> 793 <value>http://jabber.org/protocol/pubsub#subscribe_options</value> 794 </field> 795 <field var='pubsub#deliver'><value>1</value></field> 796 </x> 797 </options> 798 </pubsub> 799 </iq> 800 """ 801 802 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 803 self.assertEqual('optionsSet', request.verb) 804 self.assertEqual(JID('user@example.org'), request.sender) 805 self.assertEqual(JID('pubsub.example.org'), request.recipient) 806 self.assertEqual('test', request.nodeIdentifier) 807 self.assertEqual(JID('user@example.org/Home'), request.subscriber) 808 self.assertEqual({'pubsub#deliver': '1'}, request.options) 809 810 811 def test_fromElementOptionsSetCancel(self): 812 """ 813 Test parsing a request for cancelling setting subscription options. 814 """ 815 816 xml = """ 817 <iq type='set' to='pubsub.example.org' 818 from='user@example.org'> 819 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 820 <options node='test' jid='user@example.org/Home'> 821 <x xmlns='jabber:x:data' type='cancel'/> 822 </options> 823 </pubsub> 824 </iq> 825 """ 826 827 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 828 self.assertEqual({}, request.options) 829 830 831 def test_fromElementOptionsSetBadFormType(self): 832 """ 833 On a options set request unknown fields should be ignored. 834 """ 835 836 xml = """ 837 <iq type='set' to='pubsub.example.org' 838 from='user@example.org'> 839 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 840 <options node='test' jid='user@example.org/Home'> 841 <x xmlns='jabber:x:data' type='result'> 842 <field var='FORM_TYPE' type='hidden'> 843 <value>http://jabber.org/protocol/pubsub#node_config</value> 844 </field> 845 <field var='pubsub#deliver'><value>1</value></field> 846 </x> 847 </options> 848 </pubsub> 849 </iq> 850 """ 851 852 err = self.assertRaises(error.StanzaError, 853 pubsub.PubSubRequest.fromElement, 854 parseXml(xml)) 855 self.assertEqual('bad-request', err.condition) 856 self.assertEqual(None, err.appCondition) 857 858 859 def test_fromElementOptionsSetNoForm(self): 860 """ 861 On a options set request a form is required. 862 """ 863 864 xml = """ 865 <iq type='set' to='pubsub.example.org' 866 from='user@example.org'> 867 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 868 <options node='test' jid='user@example.org/Home'/> 869 </pubsub> 870 </iq> 871 """ 872 err = self.assertRaises(error.StanzaError, 873 pubsub.PubSubRequest.fromElement, 874 parseXml(xml)) 875 self.assertEqual('bad-request', err.condition) 876 self.assertEqual(None, err.appCondition) 877 878 879 def test_fromElementSubscriptions(self): 880 """ 881 Test parsing a request for all subscriptions. 882 """ 883 884 xml = """ 885 <iq type='get' to='pubsub.example.org' 886 from='user@example.org'> 887 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 888 <subscriptions/> 889 </pubsub> 890 </iq> 891 """ 892 893 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 894 self.assertEqual('subscriptions', request.verb) 895 self.assertEqual(JID('user@example.org'), request.sender) 896 self.assertEqual(JID('pubsub.example.org'), request.recipient) 897 898 899 def test_fromElementAffiliations(self): 900 """ 901 Test parsing a request for all affiliations. 902 """ 903 904 xml = """ 905 <iq type='get' to='pubsub.example.org' 906 from='user@example.org'> 907 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 908 <affiliations/> 909 </pubsub> 910 </iq> 911 """ 912 913 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 914 self.assertEqual('affiliations', request.verb) 915 self.assertEqual(JID('user@example.org'), request.sender) 916 self.assertEqual(JID('pubsub.example.org'), request.recipient) 917 918 919 def test_fromElementCreate(self): 920 """ 921 Test parsing a request to create a node. 922 """ 923 924 xml = """ 925 <iq type='set' to='pubsub.example.org' 926 from='user@example.org'> 927 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 928 <create node='mynode'/> 929 </pubsub> 930 </iq> 931 """ 932 933 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 934 self.assertEqual('create', request.verb) 935 self.assertEqual(JID('user@example.org'), request.sender) 936 self.assertEqual(JID('pubsub.example.org'), request.recipient) 937 self.assertEqual('mynode', request.nodeIdentifier) 938 939 940 def test_fromElementCreateInstant(self): 941 """ 942 Test parsing a request to create an instant node. 943 """ 944 945 xml = """ 946 <iq type='set' to='pubsub.example.org' 947 from='user@example.org'> 948 <pubsub xmlns='http://jabber.org/protocol/pubsub'> 949 <create/> 950 </pubsub> 951 </iq> 952 """ 953 954 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 955 self.assertIdentical(None, request.nodeIdentifier) 956 957 958 def test_fromElementDefault(self): 959 """ 960 Test parsing a request for the default node configuration. 580 961 """ 581 962 … … 589 970 """ 590 971 591 def getConfigurationOptions(): 592 return { 593 "pubsub#persist_items": 594 {"type": "boolean", 595 "label": "Persist items to storage"}, 596 "pubsub#deliver_payloads": 597 {"type": "boolean", 598 "label": "Deliver payloads with event notifications"} 599 } 600 601 def getDefaultConfiguration(requestor, service, nodeType): 602 self.assertEqual(JID('user@example.org'), requestor) 603 self.assertEqual(JID('pubsub.example.org'), service) 604 self.assertEqual('leaf', nodeType) 605 return defer.succeed({}) 606 607 def cb(element): 608 self.assertEqual('pubsub', element.name) 609 self.assertEqual(NS_PUBSUB_OWNER, element.uri) 610 self.assertEqual(NS_PUBSUB_OWNER, element.default.uri) 611 form = data_form.Form.fromElement(element.default.x) 612 self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace) 613 614 self.service.getConfigurationOptions = getConfigurationOptions 615 self.service.getDefaultConfiguration = getDefaultConfiguration 616 d = self.handleRequest(xml) 617 d.addCallback(cb) 618 return d 619 620 621 def test_onConfigureGet(self): 622 """ 623 On a node configuration get request L{PubSubService.getConfiguration} 624 is called and results in a data form with the configuration. 972 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 973 self.assertEqual('default', request.verb) 974 self.assertEqual(JID('user@example.org'), request.sender) 975 self.assertEqual(JID('pubsub.example.org'), request.recipient) 976 self.assertEqual('leaf', request.nodeType) 977 978 979 def test_fromElementDefaultCollection(self): 980 """ 981 Parsing a request for the default configuration extracts the node type. 982 """ 983 984 xml = """ 985 <iq type='get' to='pubsub.example.org' 986 from='user@example.org'> 987 <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> 988 <default> 989 <x xmlns='jabber:x:data' type='submit'> 990 <field var='FORM_TYPE' type='hidden'> 991 <value>http://jabber.org/protocol/pubsub#node_config</value> 992 </field> 993 <field var='pubsub#node_type'> 994 <value>collection</value> 995 </field> 996 </x> 997 </default> 998 999 </pubsub> 1000 </iq> 1001 """ 1002 1003 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 1004 self.assertEqual('collection', request.nodeType) 1005 1006 1007 def test_fromElementConfigureGet(self): 1008 """ 1009 Test parsing a node configuration get request. 625 1010 """ 626 1011 … … 634 1019 """ 635 1020 636 def getConfigurationOptions(): 637 return { 638 "pubsub#persist_items": 639 {"type": "boolean", 640 "label": "Persist items to storage"}, 641 "pubsub#deliver_payloads": 642 {"type": "boolean", 643 "label": "Deliver payloads with event notifications"} 644 } 645 646 def getConfiguration(requestor, service, nodeIdentifier): 647 self.assertEqual(JID('user@example.org'), requestor) 648 self.assertEqual(JID('pubsub.example.org'), service) 649 self.assertEqual('test', nodeIdentifier) 650 651 return defer.succeed({'pubsub#deliver_payloads': '0', 652 'pubsub#persist_items': '1'}) 653 654 def cb(element): 655 self.assertEqual('pubsub', element.name) 656 self.assertEqual(NS_PUBSUB_OWNER, element.uri) 657 self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri) 658 form = data_form.Form.fromElement(element.configure.x) 659 self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace) 660 fields = form.fields 661 662 self.assertIn('pubsub#deliver_payloads', fields) 663 field = fields['pubsub#deliver_payloads'] 664 self.assertEqual('boolean', field.fieldType) 665 self.assertEqual(False, field.value) 666 667 self.assertIn('pubsub#persist_items', fields) 668 field = fields['pubsub#persist_items'] 669 self.assertEqual('boolean', field.fieldType) 670 self.assertEqual(True, field.value) 671 672 self.service.getConfigurationOptions = getConfigurationOptions 673 self.service.getConfiguration = getConfiguration 674 d = self.handleRequest(xml) 675 d.addCallback(cb) 676 return d 677 678 679 def test_onConfigureSet(self): 680 """ 681 On a node configuration set request the Data Form is parsed and 682 L{PubSubService.setConfiguration} is called with the passed options. 1021 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 1022 self.assertEqual('configureGet', request.verb) 1023 self.assertEqual(JID('user@example.org'), request.sender) 1024 self.assertEqual(JID('pubsub.example.org'), request.recipient) 1025 self.assertEqual('test', request.nodeIdentifier) 1026 1027 1028 def test_fromElementConfigureSet(self): 1029 """ 1030 On a node configuration set request the Data Form is parsed. 683 1031 """ 684 1032 … … 700 1048 """ 701 1049 1050 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 1051 self.assertEqual('configureSet', request.verb) 1052 self.assertEqual(JID('user@example.org'), request.sender) 1053 self.assertEqual(JID('pubsub.example.org'), request.recipient) 1054 self.assertEqual('test', request.nodeIdentifier) 1055 self.assertEqual({'pubsub#deliver_payloads': '0', 1056 'pubsub#persist_items': '1'}, request.options) 1057 1058 1059 def test_fromElementConfigureSetCancel(self): 1060 """ 1061 The node configuration is cancelled, so no options. 1062 """ 1063 1064 xml = """ 1065 <iq type='set' to='pubsub.example.org' 1066 from='user@example.org'> 1067 <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> 1068 <configure node='test'> 1069 <x xmlns='jabber:x:data' type='cancel'/> 1070 </configure> 1071 </pubsub> 1072 </iq> 1073 """ 1074 1075 request = pubsub.PubSubRequest.fromElement(parseXml(xml)) 1076 self.assertEqual({}, request.options) 1077 1078 1079 def test_fromElementConfigureSetBadFormType(self): 1080 """ 1081