wokkel-0.7.1/ 0000775 0001750 0001750 00000000000 12074346436 013616 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/wokkel.egg-info/ 0000775 0001750 0001750 00000000000 12074346436 016604 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/wokkel.egg-info/SOURCES.txt 0000664 0001750 0001750 00000002437 12074346436 020476 0 ustar ralphm ralphm 0000000 0000000 LICENSE
MANIFEST.in
NEWS
README
setup.py
doc/examples/echo_server.tac
doc/examples/muc_client.tac
doc/examples/ping_component.tac
doc/examples/ping_s2s.tac
doc/examples/ping_server.tac
doc/examples/pinger.py
doc/examples/pinger_client.tac
doc/examples/pinger_component.tac
doc/examples/pinger_s2s.tac
doc/examples/router.tac
twisted/plugins/server.py
wokkel/__init__.py
wokkel/client.py
wokkel/compat.py
wokkel/component.py
wokkel/componentservertap.py
wokkel/data_form.py
wokkel/delay.py
wokkel/disco.py
wokkel/formats.py
wokkel/generic.py
wokkel/iwokkel.py
wokkel/muc.py
wokkel/ping.py
wokkel/pubsub.py
wokkel/server.py
wokkel/shim.py
wokkel/subprotocols.py
wokkel/xmppim.py
wokkel.egg-info/PKG-INFO
wokkel.egg-info/SOURCES.txt
wokkel.egg-info/dependency_links.txt
wokkel.egg-info/not-zip-safe
wokkel.egg-info/requires.txt
wokkel.egg-info/top_level.txt
wokkel/test/__init__.py
wokkel/test/helpers.py
wokkel/test/test_client.py
wokkel/test/test_compat.py
wokkel/test/test_component.py
wokkel/test/test_data_form.py
wokkel/test/test_delay.py
wokkel/test/test_disco.py
wokkel/test/test_generic.py
wokkel/test/test_iwokkel.py
wokkel/test/test_muc.py
wokkel/test/test_ping.py
wokkel/test/test_pubsub.py
wokkel/test/test_server.py
wokkel/test/test_shim.py
wokkel/test/test_subprotocols.py
wokkel/test/test_xmppim.py wokkel-0.7.1/wokkel.egg-info/requires.txt 0000664 0001750 0001750 00000000041 12074346436 021177 0 ustar ralphm ralphm 0000000 0000000 Twisted >= 10.0.0
python-dateutil wokkel-0.7.1/wokkel.egg-info/not-zip-safe 0000664 0001750 0001750 00000000001 11754447235 021035 0 ustar ralphm ralphm 0000000 0000000
wokkel-0.7.1/wokkel.egg-info/PKG-INFO 0000664 0001750 0001750 00000000332 12074346436 017677 0 ustar ralphm ralphm 0000000 0000000 Metadata-Version: 1.0
Name: wokkel
Version: 0.7.1
Summary: Twisted Jabber support library
Home-page: http://wokkel.ik.nu/
Author: Ralph Meijer
Author-email: ralphm@ik.nu
License: MIT
Description: UNKNOWN
Platform: any
wokkel-0.7.1/wokkel.egg-info/top_level.txt 0000664 0001750 0001750 00000000007 12074346436 021333 0 ustar ralphm ralphm 0000000 0000000 wokkel
wokkel-0.7.1/wokkel.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 12074346436 022652 0 ustar ralphm ralphm 0000000 0000000
wokkel-0.7.1/twisted/ 0000775 0001750 0001750 00000000000 12074346436 015301 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/twisted/plugins/ 0000775 0001750 0001750 00000000000 12074346436 016762 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/twisted/plugins/server.py 0000775 0001750 0001750 00000000427 11653744106 020646 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
from twisted.application.service import ServiceMaker
WokkelXMPPComponentServer = ServiceMaker(
"XMPP Component Server",
"wokkel.componentservertap",
"An XMPP Component Server",
"wokkel-component-server")
wokkel-0.7.1/setup.py 0000775 0001750 0001750 00000002515 12074331563 015331 0 ustar ralphm ralphm 0000000 0000000 #!/usr/bin/env python
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
from setuptools import setup
# Make sure 'twisted' doesn't appear in top_level.txt
try:
from setuptools.command import egg_info
egg_info.write_toplevel_names
except (ImportError, AttributeError):
pass
else:
def _top_level_package(name):
return name.split('.', 1)[0]
def _hacked_write_toplevel_names(cmd, basename, filename):
pkgs = dict.fromkeys(
[_top_level_package(k)
for k in cmd.distribution.iter_distribution_names()
if _top_level_package(k) != "twisted"
]
)
cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n')
egg_info.write_toplevel_names = _hacked_write_toplevel_names
setup(name='wokkel',
version='0.7.1',
description='Twisted Jabber support library',
author='Ralph Meijer',
author_email='ralphm@ik.nu',
maintainer_email='ralphm@ik.nu',
url='http://wokkel.ik.nu/',
license='MIT',
platforms='any',
packages=[
'wokkel',
'wokkel.test',
'twisted.plugins',
],
package_data={'twisted.plugins': ['twisted/plugins/server.py']},
zip_safe=False,
install_requires=[
'Twisted >= 10.0.0',
'python-dateutil',
],
)
wokkel-0.7.1/MANIFEST.in 0000664 0001750 0001750 00000000122 11707274572 015352 0 ustar ralphm ralphm 0000000 0000000 include NEWS
include LICENSE
include doc/examples/*.py
include doc/examples/*.tac
wokkel-0.7.1/setup.cfg 0000664 0001750 0001750 00000000073 12074346436 015437 0 ustar ralphm ralphm 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
wokkel-0.7.1/PKG-INFO 0000664 0001750 0001750 00000000332 12074346436 014711 0 ustar ralphm ralphm 0000000 0000000 Metadata-Version: 1.0
Name: wokkel
Version: 0.7.1
Summary: Twisted Jabber support library
Home-page: http://wokkel.ik.nu/
Author: Ralph Meijer
Author-email: ralphm@ik.nu
License: MIT
Description: UNKNOWN
Platform: any
wokkel-0.7.1/doc/ 0000775 0001750 0001750 00000000000 12074346436 014363 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/doc/examples/ 0000775 0001750 0001750 00000000000 12074346436 016201 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/doc/examples/pinger_s2s.tac 0000664 0001750 0001750 00000002341 12025346064 020736 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping client as a standalone server via s2s.
This ping client accepts and initiates server-to-server connections using
dialback and listens on C{127.0.1.1} with the domain set to the default
hostname of this machine.
"""
import socket
from twisted.application import service, strports
from twisted.words.protocols.jabber.jid import JID
from wokkel import component, server
from pinger import Pinger
# Configuration parameters
S2S_PORT = 'tcp:5269:interface=127.0.1.1'
SECRET = 'secret'
DOMAIN = socket.gethostname()
OTHER_DOMAIN = 'localhost'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("Pinger Server")
router = component.Router()
serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET)
serverService.logTraffic = LOG_TRAFFIC
s2sFactory = server.XMPPS2SServerFactory(serverService)
s2sFactory.logTraffic = LOG_TRAFFIC
s2sService = strports.service(S2S_PORT, s2sFactory)
s2sService.setServiceParent(application)
pingerComponent = component.InternalComponent(router, DOMAIN)
pingerComponent.logTraffic = LOG_TRAFFIC
pingerComponent.setServiceParent(application)
pingerHandler = Pinger(JID(OTHER_DOMAIN), JID(DOMAIN))
pingerHandler.setHandlerParent(pingerComponent)
wokkel-0.7.1/doc/examples/pinger_client.tac 0000644 0001750 0001750 00000001343 11322111274 021474 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping client as an XMPP client.
This pinger client logs in as C{pinger@example.org}.
"""
from twisted.application import service
from twisted.words.protocols.jabber.jid import JID
from wokkel import client
from pinger import Pinger
# Configuration parameters
THIS_JID = JID('pinger@example.org')
OTHER_JID = JID('ping.example.com')
SECRET = 'secret'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("Pinger Component")
pingerClient = client.XMPPClient(THIS_JID, SECRET)
pingerClient.logTraffic = LOG_TRAFFIC
pingerClient.setServiceParent(application)
pingerClient.send('') # Hello, OpenFire!
pingerHandler = Pinger(OTHER_JID)
pingerHandler.setHandlerParent(pingerClient)
wokkel-0.7.1/doc/examples/pinger_component.tac 0000644 0001750 0001750 00000001430 11322111274 022215 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping client as an external server-side component.
This pinger client assumes the domain C{pinger}.
"""
from twisted.application import service
from twisted.words.protocols.jabber.jid import JID
from wokkel import component
from pinger import Pinger
# Configuration parameters
EXT_HOST = 'localhost'
EXT_PORT = 5347
SECRET = 'secret'
DOMAIN = 'pinger'
OTHER_DOMAIN = 'ping'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("Pinger Component")
router = component.Router()
pingerComponent = component.Component(EXT_HOST, EXT_PORT, DOMAIN, SECRET)
pingerComponent.logTraffic = LOG_TRAFFIC
pingerComponent.setServiceParent(application)
pingerHandler = Pinger(JID(OTHER_DOMAIN), JID(DOMAIN))
pingerHandler.setHandlerParent(pingerComponent)
wokkel-0.7.1/doc/examples/ping_component.tac 0000644 0001750 0001750 00000001256 11322111274 021674 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping server as an external server-side component.
This ping server assumes the domain C{'ping'}.
"""
from twisted.application import service
from wokkel import component
from wokkel.ping import PingHandler
# Configuration parameters
EXT_HOST = 'localhost'
EXT_PORT = 5347
SECRET = 'secret'
DOMAIN = 'ping'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("Ping Component")
router = component.Router()
pingComponent = component.Component(EXT_HOST, EXT_PORT, DOMAIN, SECRET)
pingComponent.logTraffic = LOG_TRAFFIC
pingComponent.setServiceParent(application)
pingHandler = PingHandler()
pingHandler.setHandlerParent(pingComponent)
wokkel-0.7.1/doc/examples/ping_s2s.tac 0000664 0001750 0001750 00000002102 12013012404 020363 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping server as a standalone server via s2s.
This ping responder accepts and initiates server-to-server connections using
dialback and listens on C{127.0.0.1} with the domain C{localhost}.
"""
from twisted.application import service, strports
from wokkel import component, server
from wokkel.ping import PingHandler
# Configuration parameters
S2S_PORT = 'tcp:5269:interface=127.0.0.1'
SECRET = 'secret'
DOMAIN = 'localhost'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("Ping Server")
router = component.Router()
serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET)
serverService.logTraffic = LOG_TRAFFIC
s2sFactory = server.XMPPS2SServerFactory(serverService)
s2sFactory.logTraffic = LOG_TRAFFIC
s2sService = strports.service(S2S_PORT, s2sFactory)
s2sService.setServiceParent(application)
pingComponent = component.InternalComponent(router, DOMAIN)
pingComponent.logTraffic = LOG_TRAFFIC
pingComponent.setServiceParent(application)
pingHandler = PingHandler()
pingHandler.setHandlerParent(pingComponent)
wokkel-0.7.1/doc/examples/router.tac 0000644 0001750 0001750 00000001077 11322111274 020176 0 ustar ralphm ralphm 0000000 0000000 """
A generic XMPP router.
This router accepts external server-side component connections on port 5347,
but only on 127.0.0.1.
"""
from twisted.application import service, strports
from wokkel import component
application = service.Application("XMPP router")
router = component.Router()
componentServerFactory = component.XMPPComponentServerFactory(router)
componentServerFactory.logTraffic = True
componentServer = strports.service('tcp:5347:interface=127.0.0.1',
componentServerFactory)
componentServer.setServiceParent(application)
wokkel-0.7.1/doc/examples/echo_server.tac 0000644 0001750 0001750 00000007167 11322111274 021170 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP echo server as a standalone server via s2s.
This echo server accepts and initiates server-to-server connections using
dialback and listens on C{127.0.0.1} with the domain C{localhost}. It will
accept subscription requests for any potential entity at the domain and
send back messages sent to it.
"""
from twisted.application import service, strports
from twisted.words.protocols.jabber.xmlstream import toResponse
from wokkel import component, server, xmppim
# Configuration parameters
S2S_PORT = 'tcp:5269:interface=127.0.0.1'
SECRET = 'secret'
DOMAIN = 'localhost'
LOG_TRAFFIC = True
# Protocol handlers
class PresenceAcceptingHandler(xmppim.PresenceProtocol):
"""
Presence accepting XMPP subprotocol handler.
This handler blindly accepts incoming presence subscription requests,
confirms unsubscription requests and responds to presence probes.
Note that this handler does not remember any contacts, so it will not
send presence when starting.
"""
def subscribedReceived(self, presence):
"""
Subscription approval confirmation was received.
This is just a confirmation. Don't respond.
"""
pass
def unsubscribedReceived(self, presence):
"""
Unsubscription confirmation was received.
This is just a confirmation. Don't respond.
"""
pass
def subscribeReceived(self, presence):
"""
Subscription request was received.
Always grant permission to see our presence.
"""
self.subscribed(recipient=presence.sender,
sender=presence.recipient)
self.available(recipient=presence.sender,
status=u"I'm here",
sender=presence.recipient)
def unsubscribeReceived(self, presence):
"""
Unsubscription request was received.
Always confirm unsubscription requests.
"""
self.unsubscribed(recipient=presence.sender,
sender=presence.recipient)
def probeReceived(self, presence):
"""
A presence probe was received.
Always send available presence to whoever is asking.
"""
self.available(recipient=presence.sender,
status=u"I'm here",
sender=presence.recipient)
class EchoHandler(xmppim.MessageProtocol):
"""
Message echoing XMPP subprotocol handler.
"""
def onMessage(self, message):
"""
Called when a message stanza was received.
"""
# Ignore error messages
if message.getAttribute('type') == 'error':
return
# Echo incoming messages, if they have a body.
if message.body and unicode(message.body):
response = toResponse(message, message.getAttribute('type'))
response.addElement('body', content=unicode(message.body))
self.send(response)
# Set up the Twisted application
application = service.Application("Ping Server")
router = component.Router()
serverService = server.ServerService(router, domain=DOMAIN, secret=SECRET)
serverService.logTraffic = LOG_TRAFFIC
s2sFactory = server.XMPPS2SServerFactory(serverService)
s2sFactory.logTraffic = LOG_TRAFFIC
s2sService = strports.service(S2S_PORT, s2sFactory)
s2sService.setServiceParent(application)
echoComponent = component.InternalComponent(router, DOMAIN)
echoComponent.logTraffic = LOG_TRAFFIC
echoComponent.setServiceParent(application)
presenceHandler = PresenceAcceptingHandler()
presenceHandler.setHandlerParent(echoComponent)
echoHandler = EchoHandler()
echoHandler.setHandlerParent(echoComponent)
wokkel-0.7.1/doc/examples/pinger.py 0000644 0001750 0001750 00000001017 11322111274 020015 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP subprotocol handler that acts as an XMPP Ping pinger.
"""
from wokkel.ping import PingClientProtocol
class Pinger(PingClientProtocol):
"""
I send a ping as soon as I have a connection.
"""
def __init__(self, entity, sender=None):
self.entity = entity
self.sender = sender
def connectionInitialized(self):
def cb(response):
print "*** Pong ***"
print "*** Ping ***"
d = self.ping(self.entity, sender=self.sender)
d.addCallback(cb)
wokkel-0.7.1/doc/examples/ping_server.tac 0000644 0001750 0001750 00000002026 11322111274 021174 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP Ping server as a standalone server with external component service.
This ping responder server uses the C{ping} domain, and also accepts External
Component connections on port C{5347}, but only on C{127.0.0.1}.
"""
from twisted.application import service, strports
from wokkel import component
from wokkel.ping import PingHandler
# Configuration parameters
EXT_PORT = 'tcp:5347:interface=127.0.0.1'
SECRET = 'secret'
DOMAIN = 'ping'
LOG_TRAFFIC = True
# Set up the Twisted application
application = service.Application("XMPP Ping Server")
router = component.Router()
componentServerFactory = component.XMPPComponentServerFactory(router, SECRET)
componentServerFactory.logTraffic = LOG_TRAFFIC
componentServer = strports.service(EXT_PORT, componentServerFactory)
componentServer.setServiceParent(application)
pingComponent = component.InternalComponent(router, DOMAIN)
pingComponent.logTraffic = LOG_TRAFFIC
pingComponent.setServiceParent(application)
pingHandler = PingHandler()
pingHandler.setHandlerParent(pingComponent)
wokkel-0.7.1/doc/examples/muc_client.tac 0000664 0001750 0001750 00000006074 11707004455 021015 0 ustar ralphm ralphm 0000000 0000000 """
An XMPP MUC client.
This XMPP Client logs in as C{user@example.org}, joins the room
C{'room@muc.example.org'} using the nick C{'greeter'} and responds to
greetings addressed to it. If another occupant writes C{'greeter: hello'}, it
will return the favor.
This example uses L{MUCClient} instead of the protocol-only
L{MUCClientProtocol} so that it can hook into
its C{receivedGroupChat}. L{MUCClient} implements C{groupChatReceived} and
makes a distinction between messages setting the subject, messages that a
part of the room's conversation history, and 'live' messages. In this case,
we only want to inspect and respond to the 'live' messages.
"""
from twisted.application import service
from twisted.python import log
from twisted.words.protocols.jabber.jid import JID
from wokkel.client import XMPPClient
from wokkel.muc import MUCClient
# Configuration parameters
THIS_JID = JID('user@example.org')
ROOM_JID = JID('room@muc.example.org')
NICK = u'greeter'
SECRET = 'secret'
LOG_TRAFFIC = True
class MUCGreeter(MUCClient):
"""
I join a room and respond to greetings.
"""
def __init__(self, roomJID, nick):
MUCClient.__init__(self)
self.roomJID = roomJID
self.nick = nick
def connectionInitialized(self):
"""
Once authorized, join the room.
If the join action causes a new room to be created, the room will be
locked until configured. Here we will just accept the default
configuration by submitting an empty form using L{configure}, which
usually results in a public non-persistent room.
Alternatively, you would use L{getConfiguration} to retrieve the
configuration form, and then submit the filled in form with the
required settings using L{configure}, possibly after presenting it to
an end-user.
"""
def joinedRoom(room):
if room.locked:
# Just accept the default configuration.
return self.configure(room.roomJID, {})
MUCClient.connectionInitialized(self)
d = self.join(self.roomJID, self.nick)
d.addCallback(joinedRoom)
d.addCallback(lambda _: log.msg("Joined room"))
d.addErrback(log.err, "Join failed")
def receivedGroupChat(self, room, user, message):
"""
Called when a groupchat message was received.
Check if the message was addressed to my nick and if it said
C{'hello'}. Respond by sending a message to the room addressed to
the sender.
"""
if message.body.startswith(self.nick + u":"):
nick, text = message.body.split(':', 1)
text = text.strip().lower()
if text == u'hello':
body = u"%s: Hi!" % (user.nick)
self.groupChat(self.roomJID, body)
# Set up the Twisted application
application = service.Application("MUC Client")
client = XMPPClient(THIS_JID, SECRET)
client.logTraffic = LOG_TRAFFIC
client.setServiceParent(application)
mucHandler = MUCGreeter(ROOM_JID, NICK)
mucHandler.setHandlerParent(client)
wokkel-0.7.1/NEWS 0000664 0001750 0001750 00000022240 12074331547 014312 0 ustar ralphm ralphm 0000000 0000000 0.7.1 (2013-01-12)
==================
Features
--------
- wokkel.generic.Request.parseRequest is a new convenience hook for parsing
the payload of incoming requests using fromElement.
- wokkel.xmppim.RosterItem can now represent item removals and has methods
for XML (de-)serialization (#71).
- wokkel.xmppim.RosterRequest is a new class to represent roster request
stanzas (#71).
- wokkel.xmppim.RosterClientProtocol.getRoster now returns the roster
indexed by JID (#71).
- wokkel.xmppim.RosterClientProtocol uses the new RosterRequest for sending
outgoing requests, using the new request semantics (#71).
- wokkel.xmppim.RosterClientProtocol uses the new RosterRequest to provide
access to addressing and roster version information in the new callbacks
for roster pushes (#71).
- wokkel.xmppim.RosterPushIgnored can be raised for unwanted roster pushes
(#71).
- wokkel.xmppim.RosterClientProtocol and RosterRequest now support roster
versioning.
- With the new wokkel.xmppim.RosterClientProtocol.setItem roster items can
be added or updated (#56).
Fixes
-----
- wokkel.component.Component now reconnects if first attempt failed (#75).
- wokkel.xmppim.RosterClientProtocol now properly checks sender addresses
for roster pushes (#71).
- Make sure twistd plugins are installed properly (#76).
- wokkel.component.Router.route now sends back an error if there is no known
route to the stanza's destination.
- Properly encode IDN domain names for establishing client and server
connections. This resolves an issue with Twisted 12.3.0 that made it
impossible to initiate any connection using Wokkel (#77).
Deprecations
------------
- wokkel.xmppim.RosterItem.jid is deprecated in favor of entity (#71).
- wokkel.xmppim.RosterItem.ask is deprecated in favor of pendingOut (#71).
- wokkel.xmppim.RosterClientProtocol.onRosterSet is deprecated in favor of
setReceived (#71).
- wokkel.xmppim.RosterClientProtocol.onRosterRemove is deprecated in favor
of removeReceived (#71).
0.7.0 (2012-01-23)
==================
Features
--------
- Added method wokkel.data_form.Form.typeCheck for type checking incoming Data
Forms submissions against field definitions.
- Added method wokkel.data_form.Form.makeFields to add fields from a
dictionary mapping field names to values.
- Added public function wokkel.data_form.findForm for extracting Data Forms
from stanzas.
- PubSubRequest.options is now a wokkel.data_form.Form.
- wokkel.data_form.Form can now be used as a read-only dictionary.
- Added support for configuration options in Publish-Subscribe node create
requests.
- Added support for subscription options in Publish-Subscribe subscribe
requests (#63).
- Added support for Publish Subscribe subscription identifiers.
- wokkel.pubsub.Item can now be used to send out notifications, too.
- Added a twistd plugin to set up a basic XMPP server that accepts component
connections and provides server-to-server (dialback) connectivity.
- Added support for managing affiliations of Publish-Subscribe nodes,
server-side.
- Added iq request (set/get) tracking to StreamManager and provide a new base
class for such requests: wokkel.generic.Request. Unlike
twisted.words.protocols.jabber.xmlstream.IQ, Such requests can be queued
until the connection is initialized, with timeouts running from the moment
`request` was called (instead of when it was sent over the wire).
- Added support for Delayed Delivery information formats.
- Added support for XMPP Multi-User Chat, client side (#24).
Fixes
-----
- XMPP Ping handler now marks incoming ping requests as handled, so the
FallbackHandler doesn't respond, too. (#66)
- Incorporate Twisted changes for component password hashes.
- Completed test coverage for Data Forms.
- Made sure Data Forms field labels don't get overwritten (#60).
- Service Discovery identity is now reported correctly for legacy
PubSubService use (#64).
- Various smaller Service Discovery fixes for PubSubService.
- Completed test coverage for Service Discovery support.
- Publish Subscribe events with stanza type error are now ignored (#69).
- Publish Subscribe requests with multiple 'verbs' are now properly parsed
(#72).
- Publish Subscribe requests that have no legacy support in PubSubService will
now result in a feature-not-implemented error (#70).
- Publish Subscribe subscription elements now have the correct namespace when
sent out.
- Incorporated Twisted changes for passing on a reason Failure upon stream
disconnect.
- Fixed race condition and nesting issues when adding subprotocol handlers to
their StreamManager (#48).
- Reimplemented Service Discovery requests using new Request class. By reusing
common code, this fixes a problem with requests without addressing (#73).
Deprecations
------------
- wokkel.compat.BootstrapMixin is deprecated in favor of
twisted.words.xish.xmlstream.BootstrapMixin (Twisted 8.2.0).
- wokkel.compat.XmlStreamServerFactory is deprecated in favor of
twisted.words.protocols.jabber.xmlstream.XmlStreamServerFactory (Twisted
8.2.0).
- wokkel.iwokkel.IXMPPHandler is deprecated in favor of
twisted.words.protocols.jabber.ijabber.IXMPPHandler (Twisted 8.1.0).
- wokkel.iwokkel.IXMPPHandlerCollection is deprecated in favor of
twisted.words.protocols.jabber.ijabber.IXMPPHandlerCollection (Twisted
8.1.0).
- wokkel.subprotocols.XMPPHandlerCollection is deprecated in favor of
twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection (Twisted
8.1.0).
0.6.3 (2009-08-20)
==================
Features
--------
- Add a jid attribute to XMPPClient (#18).
- Add a better presence protocol handler PresenceProtocol. This handler
is also useful for component or in-server use.
Fixes
-----
- Use fallback port 5222 for failed SRV lookups for clients (#26).
0.6.2 (2009-07-08)
==================
Features
--------
- Add support for XMPP Ping (XEP-0199), doubling as example protocol
handler (#55).
- Provide examples for setting up clients, components and servers (#55).
- Make Service Discovery support accept non-deferred results from getDiscoInfo
and getDiscoItems (#55).
0.6.1 (2009-07-06)
==================
Features
--------
- Add an optional sender parameter for Service Discovery requests (#52).
Fixes:
------
- Fix regression in DeferredClientFactory (#51).
- Make IQ timeouts work with InternalComponent (#53).
0.6.0 (2009-04-22)
==================
Features
--------
- Server-to-server support, based on the dialback protocol (#33).
- Enhancement to InternalProtocol to support multiple domains (#43).
- Publish-subscribe request abstraction (#45).
- Publish-subscribe abstraction to implement a node in code (#47).
- Enhancement to PubSubClient to send requests from a specific JID (#46).
Fixes
-----
- Remove type interpretation in Data Forms field parsing code (#44).
0.5.0 (2009-04-07)
==================
This release drops support for Twisted versions older than 8.0, including
Twisted 2.5 / Twisted Words 0.5.
Features
--------
- Support for sending and receiving Publish-Subscribe node delete
notifications with redirect.
- Service Discovery client support, including an overhaul of disco data
classes (#28).
- Initial support for building XMPP servers has been added:
- XmlStreamServerFactory has been backported from Twisted Words (#29).
- An XMPP router has been added (#30).
- A server-side component authenticator has been added (#30).
- A new server-side component service, that connects to a router within the
same process, was added (#31).
Fixes
-----
- Publish-Subscribe subscriptions requests work again (#22).
- Publish-Subscribe delete node requests now have the correct namespace (#27).
- NodeIDs in Service Discovery requests are now returned in responses (#7).
- The presence of stanzaType in toResponse is now checked correctly (#34).
- Data Form fields are now rendered depending on form type (#40).
- Data Form type checking issues were addressed (#41).
- Some compatibility fixes for Twisted 8.0 and 8.1.
- Various other fixes (#37, #42) and tracking changes to code already in
Twisted.
0.4.0 (2008-08-05)
==================
- Refactoring of Data Forms support (#13).
- Added support for Stanza Headers and Internet Metadata (SHIM) (#14).
- API change for PubSubClient's methods called upon event reception (#14).
- Added client-side support for removing roster items.
- Implement type checking for data forms (#15).
- Added support for publish-subscribe collections:
- Correct handling for the root node (empty node identifier).
- Send out SHIM 'Collection' header when appropriate.
- New Subscription class for working with subscriptions.
- API change for PubSubService:
- The subscribe method returns a deferred that fires a Subscription
- The subscriptions method returns a deferred that fires a list of
Subscriptions.
- notifyPublish's notifications parameter now expects a list of tuples
that includes a list of subscriptions.
- Added PubSubService.notifyDelete to allow sending out node deletion
notifications.
0.3.1 (2008-04-22)
==================
- Fix broken version request handler.
0.3.0 (2008-04-21)
==================
First release.
wokkel-0.7.1/LICENSE 0000664 0001750 0001750 00000002046 11707274572 014630 0 ustar ralphm ralphm 0000000 0000000 Copyright (c) 2003-2012 Ralph Meijer.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
wokkel-0.7.1/wokkel/ 0000775 0001750 0001750 00000000000 12074346436 015112 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/wokkel/formats.py 0000775 0001750 0001750 00000006624 11707274572 017155 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
NS_MOOD = 'http://jabber.org/protocol/mood'
NS_TUNE = 'http://jabber.org/protocol/tune'
class Mood:
"""
User mood.
This represents a user's mood, as defined in
U{XEP-0107}.
@ivar value: The mood value.
@ivar text: The optional natural-language description of, or reason
for the mood.
"""
def __init__(self, value, text=None):
self.value = value
self.text = text
def fromXml(self, element):
"""
Get a Mood instance from an XML representation.
This class method parses the given XML document into a L{Mood}
instances.
@param element: The XML mood document.
@type element: object providing
L{IElement}
@return: A L{Mood} instance or C{None} if C{element} was not a mood
document or if there was no mood value element.
"""
if element.uri != NS_MOOD or element.name != 'mood':
return None
value = None
text = None
for child in element.elements():
if child.uri != NS_MOOD:
continue
if child.name == 'text':
text = unicode(child)
else:
value = child.name
if value:
return Mood(value, text)
else:
return None
fromXml = classmethod(fromXml)
class Tune:
"""
User tune.
This represents a user's mood, as defined in
U{XEP-0118}.
@ivar artist: The artist or performer of the song or piece.
@type artist: C{unicode}
@ivar length: The duration of the song or piece in seconds.
@type length: C{int}
@ivar source: The collection (e.g. album) or other source.
@type source: C{unicode}
@ivar title: The title of the song or piece
@type title: C{unicode}
@ivar track: A unique identifier for the tune; e.g. the track number within
the collection or the specific URI for the object.
@type track: C{unicode}
@ivar uri: A URI pointing to information about the song, collection, or
artist.
@type uri: C{str}
"""
artist = None
length = None
source = None
title = None
track = None
uri = None
def fromXml(self, element):
"""
Get a Tune instance from an XML representation.
This class method parses the given XML document into a L{Tune}
instances.
@param element: The XML tune document.
@type element: object providing
L{IElement}
@return: A L{Tune} instance or C{None} if C{element} was not a tune
document.
"""
if element.uri != NS_TUNE or element.name != 'tune':
return None
tune = Tune()
for child in element.elements():
if child.uri != NS_TUNE:
continue
if child.name in ('artist', 'source', 'title', 'track', 'uri'):
setattr(tune, child.name, unicode(child))
elif child.name == 'length':
try:
tune.length = int(unicode(child))
except ValueError:
pass
return tune
fromXml = classmethod(fromXml)
wokkel-0.7.1/wokkel/delay.py 0000664 0001750 0001750 00000006256 11707265272 016573 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_delay -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Delayed Delivery.
Support for comunicating Delayed Delivery information as specified by
U{XEP-0203} and its predecessor
U{XEP-0091}.
"""
from dateutil.parser import parse
from dateutil.tz import tzutc
from twisted.words.protocols.jabber.jid import InvalidFormat, JID
from twisted.words.xish import domish
NS_DELAY = 'urn:xmpp:delay'
NS_JABBER_DELAY = 'jabber:x:delay'
class Delay(object):
"""
Delayed Delivery information.
Instances of this class represent delayed delivery information that can be
parsed from and rendered into both XEP-0203 and legacy XEP-0091 formats.
@ivar stamp: The timestamp the stanza was originally sent.
@type stamp: L{datetime.datetime}
@ivar sender: The optional entity that originally sent the stanza or
delayed its delivery.
@type sender: L{JID}
"""
def __init__(self, stamp, sender=None):
self.stamp = stamp
self.sender = sender
def toElement(self, legacy=False):
"""
Render this instance into a domish Element.
@param legacy: If C{True}, use the legacy XEP-0091 format.
@type legacy: C{bool}
"""
if not self.stamp:
raise ValueError("stamp is required")
if self.stamp.tzinfo is None:
raise ValueError("stamp is not offset-aware")
if legacy:
element = domish.Element((NS_JABBER_DELAY, 'x'))
stampFormat = '%Y%m%dT%H:%M:%S'
else:
element = domish.Element((NS_DELAY, 'delay'))
stampFormat = '%Y-%m-%dT%H:%M:%SZ'
stamp = self.stamp.astimezone(tzutc())
element['stamp'] = stamp.strftime(stampFormat)
if self.sender:
element['from'] = self.sender.full()
return element
@staticmethod
def fromElement(element):
"""
Create an instance from a domish Element.
"""
try:
stamp = parse(element[u'stamp'])
# Assume UTC if no timezone was given
if stamp.tzinfo is None:
stamp = stamp.replace(tzinfo=tzutc())
except (KeyError, ValueError):
stamp = None
try:
sender = JID(element[u'from'])
except (KeyError, InvalidFormat):
sender = None
delay = Delay(stamp, sender)
return delay
class DelayMixin(object):
"""
Mixin for parsing delayed delivery information from stanzas.
This can be used as a mixin for subclasses of L{wokkel.generic.Stanza}
for parsing delayed delivery information. If both XEP-0203 and XEP-0091
formats are present, the former takes precedence.
"""
delay = None
childParsers = {
(NS_DELAY, 'delay'): '_childParser_delay',
(NS_JABBER_DELAY, 'x'): '_childParser_legacyDelay',
}
def _childParser_delay(self, element):
self.delay = Delay.fromElement(element)
def _childParser_legacyDelay(self, element):
if not self.delay:
self.delay = Delay.fromElement(element)
wokkel-0.7.1/wokkel/component.py 0000775 0001750 0001750 00000031512 12020400527 017452 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_component -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP External Component utilities.
"""
from twisted.application import service
from twisted.internet import reactor
from twisted.python import log
from twisted.words.protocols.jabber.jid import internJID as JID
from twisted.words.protocols.jabber import component, error, xmlstream
from twisted.words.xish import domish
from wokkel.generic import XmlPipe
from wokkel.subprotocols import StreamManager
NS_COMPONENT_ACCEPT = 'jabber:component:accept'
class Component(StreamManager, service.Service):
"""
XMPP External Component service.
This service is a XMPP stream manager that connects as an External
Component to an XMPP server, as described in
U{XEP-0114}.
"""
def __init__(self, host, port, jid, password):
self.host = host
self.port = port
factory = component.componentFactory(jid, password)
StreamManager.__init__(self, factory)
def _authd(self, xs):
"""
Called when stream initialization has completed.
This replaces the C{send} method of the C{XmlStream} instance
that represents the current connection so that outgoing stanzas
always have a from attribute set to the JID of the component.
"""
old_send = xs.send
def send(obj):
if domish.IElement.providedBy(obj) and \
not obj.getAttribute('from'):
obj['from'] = self.xmlstream.thisEntity.full()
old_send(obj)
xs.send = send
StreamManager._authd(self, xs)
def initializationFailed(self, reason):
"""
Called when stream initialization has failed.
Stop the service (thereby disconnecting the current stream) and
raise the exception.
"""
self.stopService()
reason.raiseException()
def startService(self):
"""
Start the service and connect to the server.
"""
service.Service.startService(self)
self._connection = self._getConnection()
def stopService(self):
"""
Stop the service, close the connection and prevent reconnects.
"""
service.Service.stopService(self)
self.factory.stopTrying()
self._connection.disconnect()
def _getConnection(self):
"""
Create a connector that connects to the server.
"""
return reactor.connectTCP(self.host, self.port, self.factory)
class InternalComponent(xmlstream.XMPPHandlerCollection, service.Service):
"""
Component service that connects directly to a router.
Instead of opening a socket to connect to a router, like L{Component},
components of this type connect to a router in the same process. This
allows for one-process XMPP servers.
@ivar domains: Domains (as C{str}) this component will handle traffic for.
@type domains: C{set}
"""
def __init__(self, router, domain=None):
xmlstream.XMPPHandlerCollection.__init__(self)
self._router = router
self.domains = set()
if domain:
self.domains.add(domain)
self.xmlstream = None
def startService(self):
"""
Create a XML pipe, connect to the router and setup handlers.
"""
service.Service.startService(self)
self._pipe = XmlPipe()
self.xmlstream = self._pipe.source
for domain in self.domains:
self._router.addRoute(domain, self._pipe.sink)
for e in self:
e.makeConnection(self.xmlstream)
e.connectionInitialized()
def stopService(self):
"""
Disconnect from the router and handlers.
"""
service.Service.stopService(self)
for domain in self.domains:
self._router.removeRoute(domain, self._pipe.sink)
self._pipe = None
self.xmlstream = None
for e in self:
e.connectionLost(None)
def addHandler(self, handler):
"""
Add a new handler and connect it to the stream.
"""
xmlstream.XMPPHandlerCollection.addHandler(self, handler)
if self.xmlstream:
handler.makeConnection(self.xmlstream)
handler.connectionInitialized()
def send(self, obj):
"""
Send data to the XML stream, so it ends up at the router.
"""
self.xmlstream.send(obj)
class ListenComponentAuthenticator(xmlstream.ListenAuthenticator):
"""
Authenticator for accepting components.
@ivar secret: The shared used to authorized incoming component connections.
@type secret: C{unicode}.
"""
namespace = NS_COMPONENT_ACCEPT
def __init__(self, secret):
self.secret = secret
xmlstream.ListenAuthenticator.__init__(self)
def associateWithStream(self, xs):
"""
Associate the authenticator with a stream.
This sets the stream's version to 0.0, because the XEP-0114 component
protocol was not designed for XMPP 1.0.
"""
xs.version = (0, 0)
xmlstream.ListenAuthenticator.associateWithStream(self, xs)
def streamStarted(self, rootElement):
"""
Called by the stream when it has started.
This examines the default namespace of the incoming stream and whether
there is a requested hostname for the component. Then it generates a
stream identifier, sends a response header and adds an observer for
the first incoming element, triggering L{onElement}.
"""
xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
# Compatibility fix for pre-8.2 implementations of ListenAuthenticator
if not self.xmlstream.sid:
from twisted.python import randbytes
self.xmlstream.sid = randbytes.secureRandom(8).encode('hex')
if rootElement.defaultUri != self.namespace:
exc = error.StreamError('invalid-namespace')
self.xmlstream.sendStreamError(exc)
return
# self.xmlstream.thisEntity is set to the address the component
# wants to assume.
if not self.xmlstream.thisEntity:
exc = error.StreamError('improper-addressing')
self.xmlstream.sendStreamError(exc)
return
self.xmlstream.sendHeader()
self.xmlstream.addOnetimeObserver('/*', self.onElement)
def onElement(self, element):
"""
Called on incoming XML Stanzas.
The very first element received should be a request for handshake.
Otherwise, the stream is dropped with a 'not-authorized' error. If a
handshake request was received, the hash is extracted and passed to
L{onHandshake}.
"""
if (element.uri, element.name) == (self.namespace, 'handshake'):
self.onHandshake(unicode(element))
else:
exc = error.StreamError('not-authorized')
self.xmlstream.sendStreamError(exc)
def onHandshake(self, handshake):
"""
Called upon receiving the handshake request.
This checks that the given hash in C{handshake} is equal to a
calculated hash, responding with a handshake reply or a stream error.
If the handshake was ok, the stream is authorized, and XML Stanzas may
be exchanged.
"""
calculatedHash = xmlstream.hashPassword(self.xmlstream.sid,
unicode(self.secret))
if handshake != calculatedHash:
exc = error.StreamError('not-authorized', text='Invalid hash')
self.xmlstream.sendStreamError(exc)
else:
self.xmlstream.send('')
self.xmlstream.dispatch(self.xmlstream,
xmlstream.STREAM_AUTHD_EVENT)
class Router(object):
"""
XMPP Server's Router.
A router connects the different components of the XMPP service and routes
messages between them based on the given routing table.
Connected components are trusted to have correct addressing in the
stanzas they offer for routing.
A route destination of C{None} adds a default route. Traffic for which no
specific route exists, will be routed to this default route.
@ivar routes: Routes based on the host part of JIDs. Maps host names to the
L{EventDispatcher}s that
should receive the traffic. A key of C{None} means the default route.
@type routes: C{dict}
"""
def __init__(self):
self.routes = {}
def addRoute(self, destination, xs):
"""
Add a new route.
The passed XML Stream C{xs} will have an observer for all stanzas
added to route its outgoing traffic. In turn, traffic for
C{destination} will be passed to this stream.
@param destination: Destination of the route to be added as a host name
or C{None} for the default route.
@type destination: C{str} or C{NoneType}
@param xs: XML Stream to register the route for.
@type xs:
L{EventDispatcher}
"""
self.routes[destination] = xs
xs.addObserver('/*', self.route)
def removeRoute(self, destination, xs):
"""
Remove a route.
@param destination: Destination of the route that should be removed.
@type destination: C{str}.
@param xs: XML Stream to remove the route for.
@type xs:
L{EventDispatcher}
"""
xs.removeObserver('/*', self.route)
if (xs == self.routes[destination]):
del self.routes[destination]
def route(self, stanza):
"""
Route a stanza.
@param stanza: The stanza to be routed.
@type stanza: L{domish.Element}.
"""
destination = JID(stanza['to'])
if destination.host in self.routes:
log.msg("Routing to %s: %r" % (destination.full(),
stanza.toXml()))
self.routes[destination.host].send(stanza)
elif None in self.routes:
log.msg("Routing to %s (default route): %r" % (destination.full(),
stanza.toXml()))
self.routes[None].send(stanza)
else:
log.msg("No route to %s: %r" % (destination.full(),
stanza.toXml()))
if stanza.getAttribute('type') not in ('result', 'error'):
# No route, send back error
exc = error.StanzaError('remote-server-timeout', type='wait')
exc.code = '504'
response = exc.toResponse(stanza)
self.route(response)
class XMPPComponentServerFactory(xmlstream.XmlStreamServerFactory):
"""
XMPP Component Server factory.
This factory accepts XMPP external component connections and makes
the router service route traffic for a component's bound domain
to that component.
"""
logTraffic = False
def __init__(self, router, secret='secret'):
self.router = router
self.secret = secret
def authenticatorFactory():
return ListenComponentAuthenticator(self.secret)
xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
self.makeConnection)
self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
self.connectionInitialized)
self.serial = 0
def makeConnection(self, xs):
"""
Called when a component connection was made.
This enables traffic debugging on incoming streams.
"""
xs.serial = self.serial
self.serial += 1
def logDataIn(buf):
log.msg("RECV (%d): %r" % (xs.serial, buf))
def logDataOut(buf):
log.msg("SEND (%d): %r" % (xs.serial, buf))
if self.logTraffic:
xs.rawDataInFn = logDataIn
xs.rawDataOutFn = logDataOut
xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
def connectionInitialized(self, xs):
"""
Called when a component has succesfully authenticated.
Add the component to the routing table and establish a handler
for a closed connection.
"""
destination = xs.thisEntity.host
self.router.addRoute(destination, xs)
xs.addObserver(xmlstream.STREAM_END_EVENT, self.connectionLost, 0,
destination, xs)
def onError(self, reason):
log.err(reason, "Stream Error")
def connectionLost(self, destination, xs, reason):
self.router.removeRoute(destination, xs)
wokkel-0.7.1/wokkel/componentservertap.py 0000775 0001750 0001750 00000004330 11707215356 021423 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Component Service.
This provides an XMPP server that accepts External Components connections
and accepts and initiates server-to-server connections for the specified
domain(s).
"""
from twisted.application import service, strports
from twisted.python import usage
from twisted.words.protocols.jabber import component
from wokkel import server
class Options(usage.Options):
optParameters = [
('component-port', None, 'tcp:5347:interface=127.0.0.1',
'Port components connect to'),
('component-secret', None, 'secret',
'Secret components use to connect'),
('server-port', None, 'tcp:5269',
'Port other servers connect to'),
('server-secret', None, None,
'Shared secret for dialback verification'),
]
optFlags = [
('verbose', 'v', 'Log traffic'),
]
def __init__(self):
usage.Options.__init__(self)
self['domains'] = set()
def opt_domain(self, domain):
"""
Domain to accept server connections for. Repeat for more domains.
"""
self['domains'].add(domain)
def postOptions(self):
if not self['domains']:
raise usage.UsageError('Need at least one domain')
def makeService(config):
s = service.MultiService()
router = component.Router()
# Set up the XMPP server service
serverService = server.ServerService(router, secret=config['server-secret'])
serverService.domains = config['domains']
serverService.logTraffic = config['verbose']
# Hook up XMPP server-to-server service
s2sFactory = server.XMPPS2SServerFactory(serverService)
s2sFactory.logTraffic = config['verbose']
s2sService = strports.service(config['server-port'], s2sFactory)
s2sService.setServiceParent(s)
# Hook up XMPP external server-side component service
cFactory = component.XMPPComponentServerFactory(router,
config['component-secret'])
cFactory.logTraffic = config['verbose']
cServer = strports.service(config['component-port'], cFactory)
cServer.setServiceParent(s)
return s
wokkel-0.7.1/wokkel/server.py 0000775 0001750 0001750 00000056455 12074267765 017023 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_server -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Server-to-Server protocol.
This module implements several aspects of XMPP server-to-server communications
as described in XMPP Core (RFC 3920). Refer to that document for the meaning
of the used terminology.
"""
# hashlib is new in Python 2.5, try that first.
try:
from hashlib import sha256
digestmod = sha256
except ImportError:
import Crypto.Hash.SHA256 as digestmod
sha256 = digestmod.new
import hmac
from zope.interface import implements
from twisted.internet import defer, reactor
from twisted.names.srvconnect import SRVConnector
from twisted.python import log, randbytes
from twisted.words.protocols.jabber import error, ijabber, jid, xmlstream
from twisted.words.xish import domish
from wokkel.generic import DeferredXmlStreamFactory, XmlPipe, prepareIDNName
NS_DIALBACK = 'jabber:server:dialback'
def generateKey(secret, receivingServer, originatingServer, streamID):
"""
Generate a dialback key for server-to-server XMPP Streams.
The dialback key is generated using the algorithm described in
U{XEP-0185}. The used
terminology for the parameters is described in RFC-3920.
@param secret: the shared secret known to the Originating Server and
Authoritive Server.
@type secret: C{str}
@param receivingServer: the Receiving Server host name.
@type receivingServer: C{str}
@param originatingServer: the Originating Server host name.
@type originatingServer: C{str}
@param streamID: the Stream ID as generated by the Receiving Server.
@type streamID: C{str}
@return: hexadecimal digest of the generated key.
@type: C{str}
"""
hashObject = sha256()
hashObject.update(secret)
hashedSecret = hashObject.hexdigest()
message = " ".join([receivingServer, originatingServer, streamID])
hash = hmac.HMAC(hashedSecret, message, digestmod=digestmod)
return hash.hexdigest()
def trapStreamError(xs, observer):
"""
Trap stream errors.
This wraps an observer to catch exceptions. In case of a
L{error.StreamError}, it is send over the given XML stream. All other
exceptions yield a C{'internal-server-error'} stream error, that is
sent over the stream, while the exception is logged.
@return: Wrapped observer
"""
def wrappedObserver(element):
try:
observer(element)
except error.StreamError, exc:
xs.sendStreamError(exc)
except:
log.err()
exc = error.StreamError('internal-server-error')
xs.sendStreamError(exc)
return wrappedObserver
class XMPPServerConnector(SRVConnector):
def __init__(self, reactor, domain, factory):
SRVConnector.__init__(self, reactor, 'xmpp-server', domain, factory)
def pickServer(self):
host, port = SRVConnector.pickServer(self)
if not self.servers and not self.orderedServers:
# no SRV record, fall back..
port = 5269
return host, port
class DialbackFailed(Exception):
pass
class OriginatingDialbackInitializer(object):
"""
Server Dialback Initializer for the Orginating Server.
"""
implements(ijabber.IInitiatingInitializer)
_deferred = None
def __init__(self, xs, thisHost, otherHost, secret):
self.xmlstream = xs
self.thisHost = thisHost
self.otherHost = otherHost
self.secret = secret
def initialize(self):
self._deferred = defer.Deferred()
self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT,
self.onStreamError)
self.xmlstream.addObserver("/result[@xmlns='%s']" % NS_DIALBACK,
self.onResult)
key = generateKey(self.secret, self.otherHost,
self.thisHost, self.xmlstream.sid)
result = domish.Element((NS_DIALBACK, 'result'))
result['from'] = self.thisHost
result['to'] = self.otherHost
result.addContent(key)
self.xmlstream.send(result)
return self._deferred
def onResult(self, result):
self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT,
self.onStreamError)
if result['type'] == 'valid':
self.xmlstream.otherEntity = jid.internJID(self.otherHost)
self._deferred.callback(None)
else:
self._deferred.errback(DialbackFailed())
def onStreamError(self, failure):
self.xmlstream.removeObserver("/result[@xmlns='%s']" % NS_DIALBACK,
self.onResult)
self._deferred.errback(failure)
class ReceivingDialbackInitializer(object):
"""
Server Dialback Initializer for the Receiving Server.
"""
implements(ijabber.IInitiatingInitializer)
_deferred = None
def __init__(self, xs, thisHost, otherHost, originalStreamID, key):
self.xmlstream = xs
self.thisHost = thisHost
self.otherHost = otherHost
self.originalStreamID = originalStreamID
self.key = key
def initialize(self):
self._deferred = defer.Deferred()
self.xmlstream.addObserver(xmlstream.STREAM_ERROR_EVENT,
self.onStreamError)
self.xmlstream.addObserver("/verify[@xmlns='%s']" % NS_DIALBACK,
self.onVerify)
verify = domish.Element((NS_DIALBACK, 'verify'))
verify['from'] = self.thisHost
verify['to'] = self.otherHost
verify['id'] = self.originalStreamID
verify.addContent(self.key)
self.xmlstream.send(verify)
return self._deferred
def onVerify(self, verify):
self.xmlstream.removeObserver(xmlstream.STREAM_ERROR_EVENT,
self.onStreamError)
if verify['id'] != self.originalStreamID:
self.xmlstream.sendStreamError(error.StreamError('invalid-id'))
self._deferred.errback(DialbackFailed())
elif verify['to'] != self.thisHost:
self.xmlstream.sendStreamError(error.StreamError('host-unknown'))
self._deferred.errback(DialbackFailed())
elif verify['from'] != self.otherHost:
self.xmlstream.sendStreamError(error.StreamError('invalid-from'))
self._deferred.errback(DialbackFailed())
elif verify['type'] == 'valid':
self._deferred.callback(None)
else:
self._deferred.errback(DialbackFailed())
def onStreamError(self, failure):
self.xmlstream.removeObserver("/verify[@xmlns='%s']" % NS_DIALBACK,
self.onVerify)
self._deferred.errback(failure)
class XMPPServerConnectAuthenticator(xmlstream.ConnectAuthenticator):
"""
Authenticator for an outgoing XMPP server-to-server connection.
This authenticator connects to C{otherHost} (the Receiving Server) and then
initiates dialback as C{thisHost} (the Originating Server) using
L{OriginatingDialbackInitializer}.
@ivar thisHost: The domain this server connects from (the Originating
Server) .
@ivar otherHost: The domain of the server this server connects to (the
Receiving Server).
@ivar secret: The shared secret that is used for verifying the validity
of this new connection.
"""
namespace = 'jabber:server'
def __init__(self, thisHost, otherHost, secret):
self.thisHost = thisHost
self.otherHost = otherHost
self.secret = secret
xmlstream.ConnectAuthenticator.__init__(self, otherHost)
def connectionMade(self):
self.xmlstream.thisEntity = jid.internJID(self.thisHost)
self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
NS_DIALBACK: 'db'}
xmlstream.ConnectAuthenticator.connectionMade(self)
def associateWithStream(self, xs):
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
init = OriginatingDialbackInitializer(xs, self.thisHost,
self.otherHost, self.secret)
xs.initializers = [init]
class XMPPServerVerifyAuthenticator(xmlstream.ConnectAuthenticator):
"""
Authenticator for an outgoing connection to verify an incoming connection.
This authenticator connects to C{otherHost} (the Authoritative Server) and
then initiates dialback as C{thisHost} (the Receiving Server) using
L{ReceivingDialbackInitializer}.
@ivar thisHost: The domain this server connects from (the Receiving
Server) .
@ivar otherHost: The domain of the server this server connects to (the
Authoritative Server).
@ivar originalStreamID: The stream ID of the incoming connection that is
being verified.
@ivar key: The key provided by the Receving Server to be verified.
"""
namespace = 'jabber:server'
def __init__(self, thisHost, otherHost, originalStreamID, key):
self.thisHost = thisHost
self.otherHost = otherHost
self.originalStreamID = originalStreamID
self.key = key
xmlstream.ConnectAuthenticator.__init__(self, otherHost)
def connectionMade(self):
self.xmlstream.thisEntity = jid.internJID(self.thisHost)
self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
NS_DIALBACK: 'db'}
xmlstream.ConnectAuthenticator.connectionMade(self)
def associateWithStream(self, xs):
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
init = ReceivingDialbackInitializer(xs, self.thisHost, self.otherHost,
self.originalStreamID, self.key)
xs.initializers = [init]
class XMPPServerListenAuthenticator(xmlstream.ListenAuthenticator):
"""
Authenticator for an incoming XMPP server-to-server connection.
This authenticator handles two types of incoming connections. Regular
server-to-server connections are from the Originating Server to the
Receiving Server, where this server is the Receiving Server. These
connections start out by receiving a dialback key, verifying the
key with the Authoritative Server, and then accept normal XMPP stanzas.
The other type of connections is from a Receiving Server to an
Authoritative Server, where this server acts as the Authoritative Server.
These connections are used to verify the validity of an outgoing connection
from this server. In this case, this server receives a verification
request, checks the key and then returns the result.
@ivar service: The service that keeps the list of domains we accept
connections for.
"""
namespace = 'jabber:server'
def __init__(self, service):
xmlstream.ListenAuthenticator.__init__(self)
self.service = service
def streamStarted(self, rootElement):
xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
# Compatibility fix for pre-8.2 implementations of ListenAuthenticator
if not self.xmlstream.sid:
self.xmlstream.sid = randbytes.secureRandom(8).encode('hex')
if self.xmlstream.thisEntity:
targetDomain = self.xmlstream.thisEntity.host
else:
targetDomain = self.service.defaultDomain
def prepareStream(domain):
self.xmlstream.namespace = self.namespace
self.xmlstream.prefixes = {xmlstream.NS_STREAMS: 'stream',
NS_DIALBACK: 'db'}
if domain:
self.xmlstream.thisEntity = jid.internJID(domain)
try:
if xmlstream.NS_STREAMS != rootElement.uri or \
self.namespace != self.xmlstream.namespace or \
('db', NS_DIALBACK) not in rootElement.localPrefixes.iteritems():
raise error.StreamError('invalid-namespace')
if targetDomain and targetDomain not in self.service.domains:
raise error.StreamError('host-unknown')
except error.StreamError, exc:
prepareStream(self.service.defaultDomain)
self.xmlstream.sendStreamError(exc)
return
self.xmlstream.addObserver("//verify[@xmlns='%s']" % NS_DIALBACK,
trapStreamError(self.xmlstream,
self.onVerify))
self.xmlstream.addObserver("//result[@xmlns='%s']" % NS_DIALBACK,
self.onResult)
prepareStream(targetDomain)
self.xmlstream.sendHeader()
if self.xmlstream.version >= (1, 0):
features = domish.Element((xmlstream.NS_STREAMS, 'features'))
self.xmlstream.send(features)
def onVerify(self, verify):
try:
receivingServer = jid.JID(verify['from']).host
originatingServer = jid.JID(verify['to']).host
except (KeyError, jid.InvalidFormat):
raise error.StreamError('improper-addressing')
if originatingServer not in self.service.domains:
raise error.StreamError('host-unknown')
if (self.xmlstream.otherEntity and
receivingServer != self.xmlstream.otherEntity.host):
raise error.StreamError('invalid-from')
streamID = verify.getAttribute('id', '')
key = unicode(verify)
calculatedKey = generateKey(self.service.secret, receivingServer,
originatingServer, streamID)
validity = (key == calculatedKey) and 'valid' or 'invalid'
reply = domish.Element((NS_DIALBACK, 'verify'))
reply['from'] = originatingServer
reply['to'] = receivingServer
reply['id'] = streamID
reply['type'] = validity
self.xmlstream.send(reply)
def onResult(self, result):
def reply(validity):
reply = domish.Element((NS_DIALBACK, 'result'))
reply['from'] = result['to']
reply['to'] = result['from']
reply['type'] = validity
self.xmlstream.send(reply)
def valid(xs):
reply('valid')
if not self.xmlstream.thisEntity:
self.xmlstream.thisEntity = jid.internJID(receivingServer)
self.xmlstream.otherEntity = jid.internJID(originatingServer)
self.xmlstream.dispatch(self.xmlstream,
xmlstream.STREAM_AUTHD_EVENT)
def invalid(failure):
log.err(failure)
reply('invalid')
receivingServer = result['to']
originatingServer = result['from']
key = unicode(result)
d = self.service.validateConnection(receivingServer, originatingServer,
self.xmlstream.sid, key)
d.addCallbacks(valid, invalid)
return d
class DeferredS2SClientFactory(DeferredXmlStreamFactory):
"""
Deferred firing factory for initiating XMPP server-to-server connection.
The deferred has its callbacks called upon succesful authentication with
the other server. In case of failed authentication or connection, the
deferred will have its errbacks called instead.
"""
logTraffic = False
def __init__(self, authenticator):
DeferredXmlStreamFactory.__init__(self, authenticator)
self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
self.onConnectionMade)
self.serial = 0
def onConnectionMade(self, xs):
xs.serial = self.serial
self.serial += 1
def logDataIn(buf):
log.msg("RECV (%d): %r" % (xs.serial, buf))
def logDataOut(buf):
log.msg("SEND (%d): %r" % (xs.serial, buf))
if self.logTraffic:
xs.rawDataInFn = logDataIn
xs.rawDataOutFn = logDataOut
def initiateS2S(factory):
domain = prepareIDNName(factory.authenticator.otherHost)
c = XMPPServerConnector(reactor, domain, factory)
c.connect()
return factory.deferred
class XMPPS2SServerFactory(xmlstream.XmlStreamServerFactory):
"""
XMPP Server-to-Server Server factory.
This factory accepts XMPP server-to-server connections.
"""
logTraffic = False
def __init__(self, service):
self.service = service
def authenticatorFactory():
return XMPPServerListenAuthenticator(service)
xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
self.onConnectionMade)
self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
self.onAuthenticated)
self.serial = 0
def onConnectionMade(self, xs):
"""
Called when a server-to-server connection was made.
This enables traffic debugging on incoming streams.
"""
xs.serial = self.serial
self.serial += 1
def logDataIn(buf):
log.msg("RECV (%d): %r" % (xs.serial, buf))
def logDataOut(buf):
log.msg("SEND (%d): %r" % (xs.serial, buf))
if self.logTraffic:
xs.rawDataInFn = logDataIn
xs.rawDataOutFn = logDataOut
xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
def onAuthenticated(self, xs):
thisHost = xs.thisEntity.host
otherHost = xs.otherEntity.host
log.msg("Incoming connection %d from %r to %r established" %
(xs.serial, otherHost, thisHost))
xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost,
0, xs)
xs.addObserver('/*', self.onElement, 0, xs)
def onConnectionLost(self, xs, reason):
thisHost = xs.thisEntity.host
otherHost = xs.otherEntity.host
log.msg("Incoming connection %d from %r to %r disconnected" %
(xs.serial, otherHost, thisHost))
def onError(self, reason):
log.err(reason, "Stream Error")
def onElement(self, xs, element):
"""
Called when an element was received from one of the connected streams.
"""
if element.handled:
return
else:
self.service.dispatch(xs, element)
class ServerService(object):
"""
Service for managing XMPP server to server connections.
"""
logTraffic = False
def __init__(self, router, domain=None, secret=None):
self.router = router
self.defaultDomain = domain
self.domains = set()
if self.defaultDomain:
self.domains.add(self.defaultDomain)
if secret is not None:
self.secret = secret
else:
self.secret = randbytes.secureRandom(16).encode('hex')
self._outgoingStreams = {}
self._outgoingQueues = {}
self._outgoingConnecting = set()
self.serial = 0
pipe = XmlPipe()
self.xmlstream = pipe.source
self.router.addRoute(None, pipe.sink)
self.xmlstream.addObserver('/*', self.send)
def outgoingInitialized(self, xs):
thisHost = xs.thisEntity.host
otherHost = xs.otherEntity.host
log.msg("Outgoing connection %d from %r to %r established" %
(xs.serial, thisHost, otherHost))
self._outgoingStreams[thisHost, otherHost] = xs
xs.addObserver(xmlstream.STREAM_END_EVENT,
lambda _: self.outgoingDisconnected(xs))
if (thisHost, otherHost) in self._outgoingQueues:
for element in self._outgoingQueues[thisHost, otherHost]:
xs.send(element)
del self._outgoingQueues[thisHost, otherHost]
def outgoingDisconnected(self, xs):
thisHost = xs.thisEntity.host
otherHost = xs.otherEntity.host
log.msg("Outgoing connection %d from %r to %r disconnected" %
(xs.serial, thisHost, otherHost))
del self._outgoingStreams[thisHost, otherHost]
def initiateOutgoingStream(self, thisHost, otherHost):
"""
Initiate an outgoing XMPP server-to-server connection.
"""
def resetConnecting(_):
self._outgoingConnecting.remove((thisHost, otherHost))
if (thisHost, otherHost) in self._outgoingConnecting:
return
authenticator = XMPPServerConnectAuthenticator(thisHost,
otherHost,
self.secret)
factory = DeferredS2SClientFactory(authenticator)
factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,
self.outgoingInitialized)
factory.logTraffic = self.logTraffic
self._outgoingConnecting.add((thisHost, otherHost))
d = initiateS2S(factory)
d.addBoth(resetConnecting)
return d
def validateConnection(self, thisHost, otherHost, sid, key):
"""
Validate an incoming XMPP server-to-server connection.
"""
def connected(xs):
# Set up stream for immediate disconnection.
def disconnect(_):
xs.transport.loseConnection()
xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, disconnect)
xs.addObserver(xmlstream.INIT_FAILED_EVENT, disconnect)
authenticator = XMPPServerVerifyAuthenticator(thisHost, otherHost,
sid, key)
factory = DeferredS2SClientFactory(authenticator)
factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, connected)
factory.logTraffic = self.logTraffic
d = initiateS2S(factory)
return d
def send(self, stanza):
"""
Send stanza to the proper XML Stream.
This uses addressing embedded in the stanza to find the correct stream
to forward the stanza to.
"""
otherHost = jid.internJID(stanza["to"]).host
thisHost = jid.internJID(stanza["from"]).host
if (thisHost, otherHost) not in self._outgoingStreams:
# There is no connection with the destination (yet). Cache the
# outgoing stanza until the connection has been established.
# XXX: If the connection cannot be established, the queue should
# be emptied at some point.
if (thisHost, otherHost) not in self._outgoingQueues:
self._outgoingQueues[(thisHost, otherHost)] = []
self._outgoingQueues[(thisHost, otherHost)].append(stanza)
self.initiateOutgoingStream(thisHost, otherHost)
else:
self._outgoingStreams[(thisHost, otherHost)].send(stanza)
def dispatch(self, xs, stanza):
"""
Send on element to be routed within the server.
"""
stanzaFrom = stanza.getAttribute('from')
stanzaTo = stanza.getAttribute('to')
if not stanzaFrom or not stanzaTo:
xs.sendStreamError(error.StreamError('improper-addressing'))
else:
try:
sender = jid.internJID(stanzaFrom)
jid.internJID(stanzaTo)
except jid.InvalidFormat:
log.msg("Dropping error stanza with malformed JID")
if sender.host != xs.otherEntity.host:
xs.sendStreamError(error.StreamError('invalid-from'))
else:
self.xmlstream.send(stanza)
wokkel-0.7.1/wokkel/iwokkel.py 0000775 0001750 0001750 00000106600 12074262111 017122 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_iwokkel -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Wokkel interfaces.
"""
__all__ = ['IXMPPHandler', 'IXMPPHandlerCollection',
'IPubSubClient', 'IPubSubService', 'IPubSubResource',
'IMUCClient', 'IMUCStatuses']
from zope.interface import Interface
from twisted.python.deprecate import deprecatedModuleAttribute
from twisted.python.versions import Version
from twisted.words.protocols.jabber.ijabber import IXMPPHandler
from twisted.words.protocols.jabber.ijabber import IXMPPHandlerCollection
deprecatedModuleAttribute(
Version("Wokkel", 0, 7, 0),
"Use twisted.words.protocols.jabber.ijabber.IXMPPHandler instead.",
__name__,
"IXMPPHandler")
deprecatedModuleAttribute(
Version("Wokkel", 0, 7, 0),
"Use twisted.words.protocols.jabber.ijabber.IXMPPHandlerCollection "
"instead.",
__name__,
"IXMPPHandlerCollection")
class IDisco(Interface):
"""
Interface for XMPP service discovery.
"""
def getDiscoInfo(requestor, target, nodeIdentifier=''):
"""
Get identity and features from this entity, node.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param target: The target entity to which the request is made.
@type target: L{JID}
@param nodeIdentifier: The optional identifier of the node at this
entity to retrieve the identify and features of. The default is
C{''}, meaning the root node.
@type nodeIdentifier: C{unicode}
"""
def getDiscoItems(requestor, target, nodeIdentifier=''):
"""
Get contained items for this entity, node.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param target: The target entity to which the request is made.
@type target: L{JID}
@param nodeIdentifier: The optional identifier of the node at this
entity to retrieve the identify and features of.
The default is C{''}, meaning the root node.
@type nodeIdentifier: C{unicode}
"""
class IPubSubClient(Interface):
def itemsReceived(event):
"""
Called when an items notification has been received for a node.
An item can be an element named C{item} or C{retract}. Respectively,
they signal an item being published or retracted, optionally
accompanied with an item identifier in the C{id} attribute.
@param event: The items event.
@type event: L{ItemsEvent}
"""
def deleteReceived(event):
"""
Called when a deletion notification has been received for a node.
@param event: The items event.
@type event: L{ItemsEvent}
"""
def purgeReceived(event):
"""
Called when a purge notification has been received for a node.
Upon receiving this notification all items associated should be
considered retracted.
@param event: The items event.
@type event: L{ItemsEvent}
"""
def createNode(service, nodeIdentifier=None):
"""
Create a new publish subscribe node.
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Optional suggestion for the new node's
identifier. If omitted, the creation of an
instant node will be attempted.
@type nodeIdentifier: C{unicode}
@return: a deferred that fires with the identifier of the newly created
node. Note that this can differ from the suggested identifier
if the publish subscribe service chooses to modify or ignore
the suggested identifier.
@rtype: L{Deferred}
"""
def deleteNode(service, nodeIdentifier):
"""
Delete a node.
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to be deleted.
@type nodeIdentifier: C{unicode}
@rtype: L{Deferred}
"""
def subscribe(service, nodeIdentifier, subscriber):
"""
Subscribe to a node with a given JID.
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to subscribe to.
@type nodeIdentifier: C{unicode}
@param subscriber: JID to subscribe to the node.
@type subscriber: L{JID}
@rtype: L{Deferred}
"""
def unsubscribe(service, nodeIdentifier, subscriber):
"""
Unsubscribe from a node with a given JID.
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to unsubscribe from.
@type nodeIdentifier: C{unicode}
@param subscriber: JID to unsubscribe from the node.
@type subscriber: L{JID}
@rtype: L{Deferred}
"""
def publish(service, nodeIdentifier, items=[]):
"""
Publish to a node.
Node that the C{items} parameter is optional, because so-called
transient, notification-only nodes do not use items and publish
actions only signify a change in some resource.
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to publish to.
@type nodeIdentifier: C{unicode}
@param items: List of item elements.
@type items: C{list} of L{Item}
@rtype: L{Deferred}
"""
class IPubSubService(Interface):
"""
Interface for an XMPP Publish Subscribe Service.
All methods that are called as the result of an XMPP request are to
return a deferred that fires when the requested action has been performed.
Alternatively, exceptions maybe raised directly or by calling C{errback}
on the returned deferred.
"""
def notifyPublish(service, nodeIdentifier, notifications):
"""
Send out notifications for a publish event.
@param service: The entity the notifications will originate from.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node that was published
to.
@type nodeIdentifier: C{unicode}
@param notifications: The notifications as tuples of subscriber, the
list of subscriptions and the list of items to be notified.
@type notifications: C{list} of
(L{JID}, C{list} of
L{Subscription}, C{list} of
L{Element})
"""
def notifyDelete(service, nodeIdentifier, subscribers,
redirectURI=None):
"""
Send out node deletion notifications.
@param service: The entity the notifications will originate from.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node that was deleted.
@type nodeIdentifier: C{unicode}
@param subscribers: The subscribers for which a notification should be
sent out.
@type subscribers: C{list} of
L{JID}
@param redirectURI: Optional XMPP URI of another node that subscribers
are redirected to.
@type redirectURI: C{str}
"""
def publish(requestor, service, nodeIdentifier, items):
"""
Called when a publish request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to publish to.
@type nodeIdentifier: C{unicode}
@param items: The items to be published as elements.
@type items: C{list} of C{Element}
@return: deferred that fires on success.
@rtype: L{Deferred}
"""
def subscribe(requestor, service, nodeIdentifier, subscriber):
"""
Called when a subscribe request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to subscribe to.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity to be subscribed.
@type subscriber: L{JID}
@return: A deferred that fires with a
L{Subscription}.
@rtype: L{Deferred}
"""
def unsubscribe(requestor, service, nodeIdentifier, subscriber):
"""
Called when a subscribe request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to unsubscribe from.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity to be unsubscribed.
@type subscriber: L{JID}
@return: A deferred that fires with C{None} when unsubscription has
succeeded.
@rtype: L{Deferred}
"""
def subscriptions(requestor, service):
"""
Called when a subscriptions retrieval request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@return: A deferred that fires with a C{list} of subscriptions as
L{Subscription}.
@rtype: L{Deferred}
"""
def affiliations(requestor, service):
"""
Called when a affiliations retrieval request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@return: A deferred that fires with a C{list} of affiliations as
C{tuple}s of (node identifier as C{unicode}, affiliation state as
C{str}). The affiliation can be C{'owner'}, C{'publisher'}, or
C{'outcast'}.
@rtype: L{Deferred}
"""
def create(requestor, service, nodeIdentifier):
"""
Called when a node creation request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The suggestion for the identifier of the node
to be created. If the request did not include a suggestion for the
node identifier, the value is C{None}.
@type nodeIdentifier: C{unicode} or C{NoneType}
@return: A deferred that fires with a C{unicode} that represents
the identifier of the new node.
@rtype: L{Deferred}
"""
def getConfigurationOptions():
"""
Retrieve all known node configuration options.
The returned dictionary holds the possible node configuration options
by option name. The value of each entry represents the specifics for
that option in a dictionary:
- C{'type'} (C{str}): The option's type (see
L{Field}'s doc string for possible values).
- C{'label'} (C{unicode}): A human readable label for this option.
- C{'options'} (C{dict}): Optional list of possible values for this
option.
Example::
{
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"},
"pubsub#send_last_published_item":
{"type": "list-single",
"label": "When to send the last published item",
"options": {
"never": "Never",
"on_sub": "When a new subscription is processed"}
}
}
@rtype: C{dict}.
"""
def getDefaultConfiguration(requestor, service, nodeType):
"""
Called when a default node configuration request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeType: The type of node for which the configuration is
retrieved, C{'leaf'} or C{'collection'}.
@type nodeType: C{str}
@return: A deferred that fires with a C{dict} representing the default
node configuration. Keys are C{str}s that represent the
field name. Values can be of types C{unicode}, C{int} or
C{bool}.
@rtype: L{Deferred}
"""
def getConfiguration(requestor, service, nodeIdentifier):
"""
Called when a node configuration retrieval request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to retrieve the
configuration from.
@type nodeIdentifier: C{unicode}
@return: A deferred that fires with a C{dict} representing the node
configuration. Keys are C{str}s that represent the field name.
Values can be of types C{unicode}, C{int} or C{bool}.
@rtype: L{Deferred}
"""
def setConfiguration(requestor, service, nodeIdentifier, options):
"""
Called when a node configuration change request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to change the
configuration of.
@type nodeIdentifier: C{unicode}
@return: A deferred that fires with C{None} when the node's
configuration has been changed.
@rtype: L{Deferred}
"""
def items(requestor, service, nodeIdentifier, maxItems, itemIdentifiers):
"""
Called when a items retrieval request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to retrieve items
from.
@type nodeIdentifier: C{unicode}
"""
def retract(requestor, service, nodeIdentifier, itemIdentifiers):
"""
Called when a item retraction request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to retract items
from.
@type nodeIdentifier: C{unicode}
"""
def purge(requestor, service, nodeIdentifier):
"""
Called when a node purge request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to be purged.
@type nodeIdentifier: C{unicode}
"""
def delete(requestor, service, nodeIdentifier):
"""
Called when a node deletion request has been received.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The entity the request was addressed to.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node to be delete.
@type nodeIdentifier: C{unicode}
"""
class IPubSubResource(Interface):
def locateResource(request):
"""
Locate a resource that will handle the request.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
"""
def getInfo(requestor, service, nodeIdentifier):
"""
Get node type and meta data.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to request the info for.
@type nodeIdentifier: C{unicode}
@return: A deferred that fires with a dictionary. If not empty,
it must have the keys C{'type'} and C{'meta-data'} to keep
respectively the node type and a dictionary with the meta
data for that node.
@rtype: L{Deferred}
"""
def getNodes(requestor, service, nodeIdentifier):
"""
Get all nodes contained by this node.
@param requestor: The entity the request originated from.
@type requestor: L{JID}
@param service: The publish-subscribe service entity.
@type service: L{JID}
@param nodeIdentifier: Identifier of the node to request the childs
for.
@type nodeIdentifier: C{unicode}
@return: A deferred that fires with a list of child node identifiers.
@rtype: L{Deferred}
"""
def getConfigurationOptions():
"""
Retrieve all known node configuration options.
The returned dictionary holds the possible node configuration options
by option name. The value of each entry represents the specifics for
that option in a dictionary:
- C{'type'} (C{str}): The option's type (see
L{Field}'s doc string for possible values).
- C{'label'} (C{unicode}): A human readable label for this option.
- C{'options'} (C{dict}): Optional list of possible values for this
option.
Example::
{
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"},
"pubsub#send_last_published_item":
{"type": "list-single",
"label": "When to send the last published item",
"options": {
"never": "Never",
"on_sub": "When a new subscription is processed"}
}
}
@rtype: C{dict}.
"""
def publish(request):
"""
Called when a publish request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: deferred that fires on success.
@rtype: L{Deferred}
"""
def subscribe(request):
"""
Called when a subscribe request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a
L{Subscription}.
@rtype: L{Deferred}
"""
def unsubscribe(request):
"""
Called when a subscribe request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when unsubscription has
succeeded.
@rtype: L{Deferred}
"""
def subscriptions(request):
"""
Called when a subscriptions retrieval request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{list} of subscriptions as
L{Subscription}.
@rtype: L{Deferred}
"""
def affiliations(request):
"""
Called when a affiliations retrieval request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{list} of affiliations as
C{tuple}s of (node identifier as C{unicode}, affiliation state as
C{str}). The affiliation can be C{'owner'}, C{'publisher'}, or
C{'outcast'}.
@rtype: L{Deferred}
"""
def create(request):
"""
Called when a node creation request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{unicode} that represents
the identifier of the new node.
@rtype: L{Deferred}
"""
def default(request):
"""
Called when a default node configuration request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{dict} representing the default
node configuration. Keys are C{str}s that represent the
field name. Values can be of types C{unicode}, C{int} or
C{bool}.
@rtype: L{Deferred}
"""
def configureGet(request):
"""
Called when a node configuration retrieval request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{dict} representing the node
configuration. Keys are C{str}s that represent the field name.
Values can be of types C{unicode}, C{int} or C{bool}.
@rtype: L{Deferred}
"""
def configureSet(request):
"""
Called when a node configuration change request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when the node's
configuration has been changed.
@rtype: L{Deferred}
"""
def items(request):
"""
Called when a items retrieval request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{list} of L{pubsub.Item}.
@rtype: L{Deferred}
"""
def retract(request):
"""
Called when a item retraction request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when the given items have
been retracted.
@rtype: L{Deferred}
"""
def purge(request):
"""
Called when a node purge request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when the node has been
purged.
@rtype: L{Deferred}
"""
def delete(request):
"""
Called when a node deletion request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when the node has been
deleted.
@rtype: L{Deferred}
"""
def affiliationsGet(request):
"""
Called when an owner affiliations retrieval request been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with a C{dict} of affiliations with the
entity as key (L{JID}) and
the affiliation state as value (C{unicode}). The affiliation can
be C{u'owner'}, C{u'publisher'}, or C{u'outcast'}.
@rtype: L{Deferred}
@note: Affiliations are always on the bare JID. An implementation of
this method MUST NOT return JIDs with a resource part.
"""
def affiliationsSet(request):
"""
Called when a affiliations modify request has been received.
@param request: The publish-subscribe request.
@type request: L{wokkel.pubsub.PubSubRequest}
@return: A deferred that fires with C{None} when the affiliation
changes were succesfully processed..
@rtype: L{Deferred}
@note: Affiliations are always on the bare JID. The JIDs in
L{wokkel.pubsub.PubSubRequest}'s C{affiliations} attribute are
already stripped of any resource.
"""
class IMUCClient(Interface):
"""
Multi-User Chat Client.
A client interface to XEP-045 : http://xmpp.org/extensions/xep-0045.html
"""
def receivedSubject(room, user, subject):
"""
The room subject has been received.
A subject is received when you join a room and when the subject is
changed.
@param room: The room the subject was accepted for.
@type room: L{muc.Room}
@param user: The user that set the subject.
@type user: L{muc.User}
@param subject: The subject of the given room.
@type subject: C{unicode}
"""
def receivedHistory(room, user, message):
"""
Past messages from a chat room have been received.
This occurs when you join a room.
"""
def configure(roomJID, options):
"""
Configure a room.
@param roomJID: The room to configure.
@type roomJID: L{JID}
@param options: A mapping of field names to values, or C{None} to
cancel.
@type options: C{dict}
"""
def getConfiguration(roomJID):
"""
Grab the configuration from the room.
This sends an iq request to the room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@return: A deferred that fires with the room's configuration form as
a L{data_form.Form} or C{None} if there are no configuration
options available.
"""
def join(roomJID, nick, historyOptions=None, password=None):
"""
Join a MUC room by sending presence to it.
@param roomJID: The JID of the room the entity is joining.
@type roomJID: L{JID}
@param nick: The nick name for the entitity joining the room.
@type nick: C{unicode}
@param historyOptions: Options for conversation history sent by the
room upon joining.
@type historyOptions: L{HistoryOptions}
@param password: Optional password for the room.
@type password: C{unicode}
@return: A deferred that fires when the entity is in the room or an
error has occurred.
"""
def nick(roomJID, nick):
"""
Change an entity's nick name in a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#changenick
@param roomJID: The JID of the room, i.e. without a resource.
@type roomJID: L{JID}
@param nick: The new nick name within the room.
@type nick: C{unicode}
"""
def leave(roomJID):
"""
Leave a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#exit
@param roomJID: The Room JID of the room to leave.
@type roomJID: L{JID}
"""
def userJoinedRoom(room, user):
"""
User has joined a MUC room.
This method will need to be modified inorder for clients to
do something when this event occurs.
@param room: The room the user joined.
@type room: L{muc.Room}
@param user: The user that joined the room.
@type user: L{muc.User}
"""
def groupChat(roomJID, body):
"""
Send a groupchat message.
"""
def chat(occupantJID, body):
"""
Send a private chat message to a user in a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#privatemessage
@param occupantJID: The Room JID of the other user.
@type occupantJID: L{JID}
"""
def register(roomJID, options):
"""
Send a request to register for a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param options: A mapping of field names to values, or C{None} to
cancel.
@type options: C{dict}
"""
def subject(roomJID, subject):
"""
Change the subject of a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#subject-mod
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param subject: The subject you want to set.
@type subject: C{unicode}
"""
def voice(roomJID):
"""
Request voice for a moderated room.
@param roomJID: The room jabber/xmpp entity id.
@type roomJID: L{JID}
"""
def history(roomJID, messages):
"""
Send history to create a MUC based on a one on one chat.
See: http://xmpp.org/extensions/xep-0045.html#continue
@param roomJID: The room jabber/xmpp entity id.
@type roomJID: L{JID}
@param messages: The history to send to the room as an ordered list of
message, represented by a dictionary with the keys C{'stanza'},
holding the original stanza a
L{Element}, and C{'timestamp'}
with the timestamp.
@type messages: C{list} of
L{Element}
"""
def ban(roomJID, entity, reason=None, sender=None):
"""
Ban a user from a MUC room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param entity: The bare JID of the entity to be banned.
@type entity: L{JID}
@param reason: The reason for banning the entity.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
def kick(roomJID, nick, reason=None, sender=None):
"""
Kick a user from a MUC room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param nick: The occupant to be banned.
@type nick: L{JID} or
C{unicode}
@param reason: The reason given for the kick.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
class IMUCStatuses(Interface):
"""
Interface for a container of Multi-User Chat status conditions.
"""
def __contains__(key):
"""
Return if a status exists in the container.
"""
def __iter__():
"""
Return an iterator over the status codes.
"""
def __len__():
"""
Return the number of status conditions.
"""
wokkel-0.7.1/wokkel/generic.py 0000775 0001750 0001750 00000024232 12074265150 017077 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_generic -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Generic XMPP protocol helpers.
"""
from encodings import idna
from zope.interface import implements
from twisted.internet import defer, protocol
from twisted.python import reflect
from twisted.words.protocols.jabber import error, jid, xmlstream
from twisted.words.protocols.jabber.xmlstream import toResponse
from twisted.words.xish import domish, utility
from twisted.words.xish.xmlstream import BootstrapMixin
from wokkel.iwokkel import IDisco
from wokkel.subprotocols import XMPPHandler
IQ_GET = '/iq[@type="get"]'
IQ_SET = '/iq[@type="set"]'
NS_VERSION = 'jabber:iq:version'
VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
def parseXml(string):
"""
Parse serialized XML into a DOM structure.
@param string: The serialized XML to be parsed, UTF-8 encoded.
@type string: C{str}.
@return: The DOM structure, or C{None} on empty or incomplete input.
@rtype: L{domish.Element}
"""
roots = []
results = []
elementStream = domish.elementStream()
elementStream.DocumentStartEvent = roots.append
elementStream.ElementEvent = lambda elem: roots[0].addChild(elem)
elementStream.DocumentEndEvent = lambda: results.append(roots[0])
elementStream.parse(string)
return results and results[0] or None
def stripNamespace(rootElement):
namespace = rootElement.uri
def strip(element):
if element.uri == namespace:
element.uri = None
if element.defaultUri == namespace:
element.defaultUri = None
for child in element.elements():
strip(child)
if namespace is not None:
strip(rootElement)
return rootElement
class FallbackHandler(XMPPHandler):
"""
XMPP subprotocol handler that catches unhandled iq requests.
Unhandled iq requests are replied to with a service-unavailable stanza
error.
"""
def connectionInitialized(self):
self.xmlstream.addObserver(IQ_SET, self.iqFallback, -1)
self.xmlstream.addObserver(IQ_GET, self.iqFallback, -1)
def iqFallback(self, iq):
if iq.handled == True:
return
reply = error.StanzaError('service-unavailable')
self.xmlstream.send(reply.toResponse(iq))
class VersionHandler(XMPPHandler):
"""
XMPP subprotocol handler for XMPP Software Version.
This protocol is described in
U{XEP-0092}.
"""
implements(IDisco)
def __init__(self, name, version):
self.name = name
self.version = version
def connectionInitialized(self):
self.xmlstream.addObserver(VERSION, self.onVersion)
def onVersion(self, iq):
response = toResponse(iq, "result")
query = response.addElement((NS_VERSION, "query"))
query.addElement("name", content=self.name)
query.addElement("version", content=self.version)
self.send(response)
iq.handled = True
def getDiscoInfo(self, requestor, target, node):
info = set()
if not node:
from wokkel import disco
info.add(disco.DiscoFeature(NS_VERSION))
return defer.succeed(info)
def getDiscoItems(self, requestor, target, node):
return defer.succeed([])
class XmlPipe(object):
"""
XML stream pipe.
Connects two objects that communicate stanzas through an XML stream like
interface. Each of the ends of the pipe (sink and source) can be used to
send XML stanzas to the other side, or add observers to process XML stanzas
that were sent from the other side.
XML pipes are usually used in place of regular XML streams that are
transported over TCP. This is the reason for the use of the names source
and sink for both ends of the pipe. The source side corresponds with the
entity that initiated the TCP connection, whereas the sink corresponds with
the entity that accepts that connection. In this object, though, the source
and sink are treated equally.
Unlike Jabber
L{XmlStream}s, the sink
and source objects are assumed to represent an eternal connected and
initialized XML stream. As such, events corresponding to connection,
disconnection, initialization and stream errors are not dispatched or
processed.
@ivar source: Source XML stream.
@ivar sink: Sink XML stream.
"""
def __init__(self):
self.source = utility.EventDispatcher()
self.sink = utility.EventDispatcher()
self.source.send = lambda obj: self.sink.dispatch(obj)
self.sink.send = lambda obj: self.source.dispatch(obj)
class Stanza(object):
"""
Abstract representation of a stanza.
@ivar sender: The sending entity.
@type sender: L{jid.JID}
@ivar recipient: The receiving entity.
@type recipient: L{jid.JID}
"""
recipient = None
sender = None
stanzaKind = None
stanzaID = None
stanzaType = None
def __init__(self, recipient=None, sender=None):
self.recipient = recipient
self.sender = sender
@classmethod
def fromElement(Class, element):
"""
Create a stanza from a L{domish.Element}.
"""
stanza = Class()
stanza.parseElement(element)
return stanza
def parseElement(self, element):
"""
Parse the stanza element.
This is called with the stanza's element when a L{Stanza} is
created using L{fromElement}. It parses the stanza's core attributes
(addressing, type and id), strips the namespace from the stanza
element for easier transport across streams and passes on
child elements for further parsing.
Child element parsers are defined by providing a C{childParsers}
attribute on a subclass, as a mapping from (URI, name) to the name
of the handler on C{self}. C{parseElement} will accumulate
C{childParsers} from its class hierarchy, iterate over the child
elements and pass it to matching handlers based on the child element's
URI and name. The special key of C{None} can be used to pass all
child elements to.
"""
if element.hasAttribute('from'):
self.sender = jid.internJID(element['from'])
if element.hasAttribute('to'):
self.recipient = jid.internJID(element['to'])
self.stanzaType = element.getAttribute('type')
self.stanzaID = element.getAttribute('id')
# Save element
stripNamespace(element)
self.element = element
# accumulate all childHandlers in the class hierarchy of Class
handlers = {}
reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
for child in element.elements():
try:
handler = handlers[child.uri, child.name]
except KeyError:
try:
handler = handlers[None]
except KeyError:
continue
getattr(self, handler)(child)
def toElement(self):
element = domish.Element((None, self.stanzaKind))
if self.sender is not None:
element['from'] = self.sender.full()
if self.recipient is not None:
element['to'] = self.recipient.full()
if self.stanzaType:
element['type'] = self.stanzaType
if self.stanzaID:
element['id'] = self.stanzaID
return element
class ErrorStanza(Stanza):
def parseElement(self, element):
Stanza.parseElement(self, element)
self.exception = error.exceptionFromStanza(element)
class Request(Stanza):
"""
IQ request stanza.
This is a base class for IQ get or set stanzas, to be used with
L{wokkel.subprotocols.StreamManager.request}.
"""
stanzaKind = 'iq'
stanzaType = 'get'
timeout = None
childParsers = {None: 'parseRequest'}
def __init__(self, recipient=None, sender=None, stanzaType='get'):
Stanza.__init__(self, recipient=recipient, sender=sender)
self.stanzaType = stanzaType
def parseRequest(self, element):
"""
Called with the request's child element for parsing.
When a request instance is created using L{fromElement}, this method
is called with the child element of the iq. Override this method for
parsing the request's payload.
"""
def toElement(self):
element = Stanza.toElement(self)
if not self.stanzaID:
element.addUniqueId()
self.stanzaID = element['id']
return element
class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
protocol = xmlstream.XmlStream
def __init__(self, authenticator):
BootstrapMixin.__init__(self)
self.authenticator = authenticator
deferred = defer.Deferred()
self.deferred = deferred
self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.deferred.callback)
self.addBootstrap(xmlstream.INIT_FAILED_EVENT, deferred.errback)
def buildProtocol(self, addr):
"""
Create an instance of XmlStream.
A new authenticator instance will be created and passed to the new
XmlStream. Registered bootstrap event observers are installed as well.
"""
xs = self.protocol(self.authenticator)
xs.factory = self
self.installBootstraps(xs)
return xs
def clientConnectionFailed(self, connector, reason):
self.deferred.errback(reason)
def prepareIDNName(name):
"""
Encode a unicode IDN Domain Name into its ACE equivalent.
This will encode the domain labels, separated by allowed dot code points,
to their ASCII Compatible Encoding (ACE) equivalent, using punycode. The
result is an ASCII byte string of the encoded labels, separated by the
standard full stop.
"""
result = []
labels = idna.dots.split(name)
if labels and len(labels[-1]) == 0:
trailing_dot = b'.'
del labels[-1]
else:
trailing_dot = b''
for label in labels:
result.append(idna.ToASCII(label))
return b'.'.join(result) + trailing_dot
wokkel-0.7.1/wokkel/client.py 0000775 0001750 0001750 00000012257 12074266455 016756 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_client -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Client support.
This module holds several improvements on top of Twisted's XMPP support
that should probably eventually move there.
"""
from twisted.application import service
from twisted.internet import reactor
from twisted.names.srvconnect import SRVConnector
from twisted.words.protocols.jabber import client, sasl, xmlstream
from wokkel import generic
from wokkel.subprotocols import StreamManager
class CheckAuthInitializer(object):
"""
Check what authentication methods are available.
"""
def __init__(self, xs):
self.xmlstream = xs
def initialize(self):
if (sasl.NS_XMPP_SASL, 'mechanisms') in self.xmlstream.features:
inits = [(sasl.SASLInitiatingInitializer, True),
(client.BindInitializer, True),
(client.SessionInitializer, False)]
for initClass, required in inits:
init = initClass(self.xmlstream)
init.required = required
self.xmlstream.initializers.append(init)
elif (client.NS_IQ_AUTH_FEATURE, 'auth') in self.xmlstream.features:
self.xmlstream.initializers.append(
client.IQAuthInitializer(self.xmlstream))
else:
raise Exception("No available authentication method found")
class HybridAuthenticator(xmlstream.ConnectAuthenticator):
"""
Initializes an XmlStream connecting to an XMPP server as a Client.
This is similar to L{client.XMPPAuthenticator}, but also tries non-SASL
autentication.
"""
namespace = 'jabber:client'
def __init__(self, jid, password):
xmlstream.ConnectAuthenticator.__init__(self, jid.host)
self.jid = jid
self.password = password
def associateWithStream(self, xs):
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
tlsInit = xmlstream.TLSInitiatingInitializer(xs)
xs.initializers = [client.CheckVersionInitializer(xs),
tlsInit,
CheckAuthInitializer(xs)]
def HybridClientFactory(jid, password):
"""
Client factory for XMPP 1.0.
This is similar to L{client.XMPPClientFactory} but also tries non-SASL
autentication.
"""
a = HybridAuthenticator(jid, password)
return xmlstream.XmlStreamFactory(a)
class XMPPClient(StreamManager, service.Service):
"""
Service that initiates an XMPP client connection.
"""
def __init__(self, jid, password, host=None, port=5222):
self.jid = jid
self.domain = generic.prepareIDNName(jid.host)
self.host = host
self.port = port
factory = HybridClientFactory(jid, password)
StreamManager.__init__(self, factory)
def startService(self):
service.Service.startService(self)
self._connection = self._getConnection()
def stopService(self):
service.Service.stopService(self)
self.factory.stopTrying()
self._connection.disconnect()
def _authd(self, xs):
"""
Called when the stream has been initialized.
Save the JID that we were assigned by the server, as the resource might
differ from the JID we asked for. This is stored on the authenticator
by its constituent initializers.
"""
self.jid = self.factory.authenticator.jid
StreamManager._authd(self, xs)
def initializationFailed(self, reason):
"""
Called when stream initialization has failed.
Stop the service (thereby disconnecting the current stream) and
raise the exception.
"""
self.stopService()
reason.raiseException()
def _getConnection(self):
if self.host:
return reactor.connectTCP(self.host, self.port, self.factory)
else:
c = XMPPClientConnector(reactor, self.domain, self.factory)
c.connect()
return c
class DeferredClientFactory(generic.DeferredXmlStreamFactory):
def __init__(self, jid, password):
authenticator = client.XMPPAuthenticator(jid, password)
generic.DeferredXmlStreamFactory.__init__(self, authenticator)
self.streamManager = StreamManager(self)
def addHandler(self, handler):
"""
Add a subprotocol handler to the stream manager.
"""
self.streamManager.addHandler(handler)
def removeHandler(self, handler):
"""
Add a subprotocol handler to the stream manager.
"""
self.streamManager.removeHandler(handler)
class XMPPClientConnector(SRVConnector):
def __init__(self, reactor, domain, factory):
SRVConnector.__init__(self, reactor, 'xmpp-client', domain, factory)
def pickServer(self):
host, port = SRVConnector.pickServer(self)
if not self.servers and not self.orderedServers:
# no SRV record, fall back..
port = 5222
return host, port
def clientCreator(factory):
domain = generic.prepareIDNName(factory.authenticator.jid.host)
c = XMPPClientConnector(reactor, domain, factory)
c.connect()
return factory.deferred
wokkel-0.7.1/wokkel/subprotocols.py 0000775 0001750 0001750 00000036405 12074262111 020220 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_subprotocols -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
XMPP subprotocol support.
"""
__all__ = ['XMPPHandler', 'XMPPHandlerCollection', 'StreamManager',
'IQHandlerMixin']
from zope.interface import implements
from twisted.internet import defer
from twisted.internet.error import ConnectionDone
from twisted.python import failure, log
from twisted.python.deprecate import deprecatedModuleAttribute
from twisted.python.versions import Version
from twisted.words.protocols.jabber import error, ijabber, xmlstream
from twisted.words.protocols.jabber.xmlstream import toResponse
from twisted.words.protocols.jabber.xmlstream import XMPPHandlerCollection
from twisted.words.xish import xpath
from twisted.words.xish.domish import IElement
deprecatedModuleAttribute(
Version("Wokkel", 0, 7, 0),
"Use twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection "
"instead.",
__name__,
"XMPPHandlerCollection")
class XMPPHandler(object):
"""
XMPP protocol handler.
Classes derived from this class implement (part of) one or more XMPP
extension protocols, and are referred to as a subprotocol implementation.
"""
implements(ijabber.IXMPPHandler)
def __init__(self):
self.parent = None
self.xmlstream = None
def setHandlerParent(self, parent):
self.parent = parent
self.parent.addHandler(self)
def disownHandlerParent(self, parent):
self.parent.removeHandler(self)
self.parent = None
def makeConnection(self, xs):
self.xmlstream = xs
self.connectionMade()
def connectionMade(self):
"""
Called after a connection has been established.
Can be overridden to perform work before stream initialization.
"""
def connectionInitialized(self):
"""
The XML stream has been initialized.
Can be overridden to perform work after stream initialization, e.g. to
set up observers and start exchanging XML stanzas.
"""
def connectionLost(self, reason):
"""
The XML stream has been closed.
This method can be extended to inspect the C{reason} argument and
act on it.
"""
self.xmlstream = None
def send(self, obj):
"""
Send data over the managed XML stream.
@note: The stream manager maintains a queue for data sent using this
method when there is no current initialized XML stream. This
data is then sent as soon as a new stream has been established
and initialized. Subsequently, L{connectionInitialized} will be
called again. If this queueing is not desired, use C{send} on
C{self.xmlstream}.
@param obj: data to be sent over the XML stream. This is usually an
object providing L{domish.IElement}, or serialized XML. See
L{xmlstream.XmlStream} for details.
"""
self.parent.send(obj)
def request(self, request):
"""
Send an IQ request and track the response.
This passes the request to the parent for sending and response
tracking.
@see: L{StreamManager.request}.
"""
return self.parent.request(request)
class StreamManager(XMPPHandlerCollection):
"""
Business logic representing a managed XMPP connection.
This maintains a single XMPP connection and provides facilities for packet
routing and transmission. Business logic modules are objects providing
L{IXMPPHandler} (like subclasses of L{XMPPHandler}), and added
using L{addHandler}.
@ivar xmlstream: currently managed XML stream
@type xmlstream: L{XmlStream}
@ivar logTraffic: if true, log all traffic.
@type logTraffic: C{bool}
@ivar _initialized: Whether the stream represented by L{xmlstream} has
been initialized. This is used when caching outgoing
stanzas.
@type _initialized: C{bool}
@ivar _packetQueue: internal buffer of unsent data. See L{send} for details.
@type _packetQueue: L{list}
@ivar timeout: Default IQ request timeout in seconds.
@type timeout: C{int}
@ivar _reactor: A provider of L{IReactorTime} to track timeouts.
"""
timeout = None
_reactor = None
logTraffic = False
def __init__(self, factory, reactor=None):
"""
Construct a stream manager.
@param factory: The stream factory to connect with.
@param reactor: A provider of L{IReactorTime} to track timeouts.
If not provided, the global reactor will be used.
"""
XMPPHandlerCollection.__init__(self)
self.xmlstream = None
self._packetQueue = []
self._initialized = False
factory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self._connected)
factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd)
factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,
self.initializationFailed)
factory.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected)
self.factory = factory
if reactor is None:
from twisted.internet import reactor
self._reactor = reactor
# Set up IQ response tracking
self._iqDeferreds = {}
def addHandler(self, handler):
"""
Add protocol handler.
When an XML stream has already been established, the handler's
C{connectionInitialized} will be called to get it up to speed.
"""
XMPPHandlerCollection.addHandler(self, handler)
# get protocol handler up to speed when a connection has already
# been established
if self.xmlstream:
handler.makeConnection(self.xmlstream)
if self._initialized:
handler.connectionInitialized()
def _connected(self, xs):
"""
Called when the transport connection has been established.
Here we optionally set up traffic logging (depending on L{logTraffic})
and call each handler's C{makeConnection} method with the L{XmlStream}
instance.
"""
def logDataIn(buf):
log.msg("RECV: %r" % buf)
def logDataOut(buf):
log.msg("SEND: %r" % buf)
if self.logTraffic:
xs.rawDataInFn = logDataIn
xs.rawDataOutFn = logDataOut
self.xmlstream = xs
for e in list(self):
e.makeConnection(xs)
def _authd(self, xs):
"""
Called when the stream has been initialized.
Send out cached stanzas and call each handler's
C{connectionInitialized} method.
"""
xs.addObserver('/iq[@type="result"]', self._onIQResponse)
xs.addObserver('/iq[@type="error"]', self._onIQResponse)
# Flush all pending packets
for p in self._packetQueue:
xs.send(p)
self._packetQueue = []
self._initialized = True
# Notify all child services which implement
# the IService interface
for e in list(self):
e.connectionInitialized()
def initializationFailed(self, reason):
"""
Called when stream initialization has failed.
Stream initialization has halted, with the reason indicated by
C{reason}. It may be retried by calling the authenticator's
C{initializeStream}. See the respective authenticators for details.
@param reason: A failure instance indicating why stream initialization
failed.
@type reason: L{failure.Failure}
"""
def _disconnected(self, reason):
"""
Called when the stream has been closed.
From this point on, the manager doesn't interact with the
L{XmlStream} anymore and notifies each handler that the connection
was lost by calling its C{connectionLost} method.
"""
self.xmlstream = None
self._initialized = False
# Twisted versions before 11.0 passed an XmlStream here.
if not hasattr(reason, 'trap'):
reason = failure.Failure(ConnectionDone())
# Notify all child services which implement
# the IService interface
for e in list(self):
e.connectionLost(reason)
# This errbacks all deferreds of iq's for which no response has
# been received with a L{ConnectionLost} failure. Otherwise, the
# deferreds will never be fired.
iqDeferreds = self._iqDeferreds
self._iqDeferreds = {}
for d in iqDeferreds.itervalues():
d.errback(reason)
def _onIQResponse(self, iq):
"""
Handle iq response by firing associated deferred.
"""
try:
d = self._iqDeferreds[iq["id"]]
except KeyError:
return
del self._iqDeferreds[iq["id"]]
iq.handled = True
if iq['type'] == 'error':
d.errback(error.exceptionFromStanza(iq))
else:
d.callback(iq)
def send(self, obj):
"""
Send data over the XML stream.
When there is no established XML stream, the data is queued and sent
out when a new XML stream has been established and initialized.
@param obj: data to be sent over the XML stream. See
L{xmlstream.XmlStream.send} for details.
"""
if self._initialized:
self.xmlstream.send(obj)
else:
self._packetQueue.append(obj)
def request(self, request):
"""
Send an IQ request and track the response.
A request is an IQ L{generic.Stanza} of type C{'get'} or C{'set'}. It
will have its C{toElement} called to render to a
L{Element} which is then sent out
over the current stream. If there is no such stream (yet), it is queued
and sent whenever a connection is established and initialized, just
like L{send}.
If the request doesn't have an identifier, it will be assigned a fresh
one, so the response can be tracked.
The deferred that is returned will fire with the
L{Element} representation of the
response if it is a result iq. If the response is an error iq, a
corresponding L{error.StanzaError} will be errbacked.
If the connection is closed before a response was received, the deferred
will be errbacked with the reason failure.
A request may also have a timeout, either by setting a default timeout
in L{StreamManager}'s C{timeout} attribute or on the C{timeout}
attribute of the request.
@param request: The IQ request.
@type request: L{generic.Request}
"""
if (request.stanzaKind != 'iq' or
request.stanzaType not in ('get', 'set')):
return defer.fail(ValueError("Not a request"))
element = request.toElement()
# Make sure we have a trackable id on the stanza
if not request.stanzaID:
element.addUniqueId()
request.stanzaID = element['id']
# Set up iq response tracking
d = defer.Deferred()
self._iqDeferreds[element['id']] = d
timeout = getattr(request, 'timeout', self.timeout)
if timeout is not None:
def onTimeout():
del self._iqDeferreds[element['id']]
d.errback(xmlstream.TimeoutError("IQ timed out"))
call = self._reactor.callLater(timeout, onTimeout)
def cancelTimeout(result):
if call.active():
call.cancel()
return result
d.addBoth(cancelTimeout)
self.send(element)
return d
class IQHandlerMixin(object):
"""
XMPP subprotocol mixin for handle incoming IQ stanzas.
This matches up the iq with XPath queries to call methods on itself,
wrapping the call so that exceptions result in proper error responses,
or, when succesful will reply with a response with optional payload.
Derivatives of this class must provide an
L{XmlStream} instance
in its C{xmlstream} attribute.
The optional payload is taken from the result of the handler and is
expected to be a child or a list of childs.
If an exception is raised, or the deferred has its errback called,
the exception is checked for being a L{error.StanzaError}. If so,
an error response is sent. Any other exception will cause a error
response of C{internal-server-error} to be sent.
A typical way to use this mixin, is to set up L{xpath} observers on the
C{xmlstream} to call handleRequest, for example in an overridden
L{XMPPHandler.connectionMade}. It is likely a good idea to only listen for
incoming iq get and/org iq set requests, and not for any iq, to prevent
hijacking incoming responses to outgoing iq requests. An example:
>>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']"
>>> class MyHandler(XMPPHandler, IQHandlerMixin):
... iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet',
... "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'}
... def connectionMade(self):
... self.xmlstream.addObserver(
... "/iq[@type='get' or @type='set']" + QUERY_ROSTER,
... self.handleRequest)
... def onRosterGet(self, iq):
... pass
... def onRosterSet(self, iq):
... pass
@cvar iqHandlers: Mapping from XPath queries (as a string) to the method
name that will handle requests that match the query.
@type iqHandlers: C{dict}
"""
iqHandlers = None
def handleRequest(self, iq):
"""
Find a handler and wrap the call for sending a response stanza.
"""
def toResult(result, iq):
response = toResponse(iq, 'result')
if result:
if IElement.providedBy(result):
response.addChild(result)
else:
for element in result:
response.addChild(element)
return response
def checkNotImplemented(failure):
failure.trap(NotImplementedError)
raise error.StanzaError('feature-not-implemented')
def fromStanzaError(failure, iq):
failure.trap(error.StanzaError)
return failure.value.toResponse(iq)
def fromOtherError(failure, iq):
log.msg("Unhandled error in iq handler:", isError=True)
log.err(failure)
return error.StanzaError('internal-server-error').toResponse(iq)
handler = None
for queryString, method in self.iqHandlers.iteritems():
if xpath.internQuery(queryString).matches(iq):
handler = getattr(self, method)
if handler:
d = defer.maybeDeferred(handler, iq)
else:
d = defer.fail(NotImplementedError())
d.addCallback(toResult, iq)
d.addErrback(checkNotImplemented)
d.addErrback(fromStanzaError, iq)
d.addErrback(fromOtherError, iq)
d.addCallback(self.send)
iq.handled = True
wokkel-0.7.1/wokkel/ping.py 0000775 0001750 0001750 00000005677 11707274572 016446 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_ping -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Ping.
The XMPP Ping protocol is documented in
U{XEP-0199}.
"""
from zope.interface import implements
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.protocols.jabber.xmlstream import IQ, toResponse
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from wokkel import disco, iwokkel
NS_PING = 'urn:xmpp:ping'
PING_REQUEST = "/iq[@type='get']/ping[@xmlns='%s']" % NS_PING
class PingClientProtocol(XMPPHandler):
"""
Ping client.
This handler implements the protocol for sending out XMPP Ping requests.
"""
def ping(self, entity, sender=None):
"""
Send out a ping request and wait for a response.
@param entity: Entity to be pinged.
@type entity: L{JID}
@return: A deferred that fires upon receiving a response.
@rtype: L{Deferred}
@param sender: Optional sender address.
@type sender: L{JID}
"""
def cb(response):
return None
def eb(failure):
failure.trap(StanzaError)
exc = failure.value
if exc.condition == 'service-unavailable':
return None
else:
return failure
request = IQ(self.xmlstream, 'get')
request.addElement((NS_PING, 'ping'))
if sender is not None:
request['from'] = unicode(sender)
d = request.send(entity.full())
d.addCallbacks(cb, eb)
return d
class PingHandler(XMPPHandler):
"""
Ping responder.
This handler waits for XMPP Ping requests and sends a response.
"""
implements(iwokkel.IDisco)
def connectionInitialized(self):
"""
Called when the XML stream has been initialized.
This sets up an observer for incoming ping requests.
"""
self.xmlstream.addObserver(PING_REQUEST, self.onPing)
def onPing(self, iq):
"""
Called when a ping request has been received.
This immediately replies with a result response.
"""
response = toResponse(iq, 'result')
self.xmlstream.send(response)
iq.handled = True
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
"""
Get identity and features from this entity, node.
This handler supports XMPP Ping, but only without a nodeIdentifier
specified.
"""
if not nodeIdentifier:
return [disco.DiscoFeature(NS_PING)]
else:
return []
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
"""
Get contained items for this entity, node.
This handler does not support items.
"""
return []
wokkel-0.7.1/wokkel/compat.py 0000775 0001750 0001750 00000022426 11707274572 016763 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_compat -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Compatibility module to provide backwards compatibility with Twisted features.
"""
__all__ = ['BootstrapMixin', 'XmlStreamServerFactory', 'IQ',
'NamedConstant', 'ValueConstant', 'Names', 'Values']
from itertools import count
from twisted.python.deprecate import deprecatedModuleAttribute
from twisted.python.versions import Version
from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber.xmlstream import XmlStreamServerFactory
from twisted.words.xish.xmlstream import BootstrapMixin
deprecatedModuleAttribute(
Version("Wokkel", 0, 7, 0),
"Use twisted.words.xish.xmlstream.BootstrapMixin instead.",
__name__,
"BootstrapMixin")
deprecatedModuleAttribute(
Version("Wokkel", 0, 7, 0),
"Use twisted.words.protocols.jabber.xmlstream.XmlStreamServerFactory "
"instead.",
__name__,
"XmlStreamServerFactory")
class IQ(xmlstream.IQ):
def __init__(self, *args, **kwargs):
# Make sure we have a reactor parameter
try:
reactor = kwargs['reactor']
except KeyError:
from twisted.internet import reactor
kwargs['reactor'] = reactor
# Check if IQ's init accepts the reactor parameter
try:
xmlstream.IQ.__init__(self, *args, **kwargs)
except TypeError:
# Guess not. Remove the reactor parameter and try again.
del kwargs['reactor']
xmlstream.IQ.__init__(self, *args, **kwargs)
# Patch the XmlStream instance so that it has a _callLater
self._xmlstream._callLater = reactor.callLater
_unspecified = object()
_constantOrder = count().next
class _Constant(object):
"""
@ivar _index: A C{int} allocated from a shared counter in order to keep
track of the order in which L{_Constant}s are instantiated.
@ivar name: A C{str} giving the name of this constant; only set once the
constant is initialized by L{_ConstantsContainer}.
@ivar _container: The L{_ConstantsContainer} subclass this constant belongs
to; only set once the constant is initialized by that subclass.
@since: Twisted 12.0.0.
"""
def __init__(self):
self._index = _constantOrder()
def __get__(self, oself, cls):
"""
Ensure this constant has been initialized before returning it.
"""
cls._initializeEnumerants()
return self
def __repr__(self):
"""
Return text identifying both which constant this is and which collection
it belongs to.
"""
return "<%s=%s>" % (self._container.__name__, self.name)
def _realize(self, container, name, value):
"""
Complete the initialization of this L{_Constant}.
@param container: The L{_ConstantsContainer} subclass this constant is
part of.
@param name: The name of this constant in its container.
@param value: The value of this constant; not used, as named constants
have no value apart from their identity.
"""
self._container = container
self.name = name
class _EnumerantsInitializer(object):
"""
L{_EnumerantsInitializer} is a descriptor used to initialize a cache of
objects representing named constants for a particular L{_ConstantsContainer}
subclass.
@since: Twisted 12.0.0.
"""
def __get__(self, oself, cls):
"""
Trigger the initialization of the enumerants cache on C{cls} and then
return it.
"""
cls._initializeEnumerants()
return cls._enumerants
class _ConstantsContainer(object):
"""
L{_ConstantsContainer} is a class with attributes used as symbolic
constants. It is up to subclasses to specify what kind of constants are
allowed.
@cvar _constantType: Specified by a L{_ConstantsContainer} subclass to
specify the type of constants allowed by that subclass.
@cvar _enumerantsInitialized: A C{bool} tracking whether C{_enumerants} has
been initialized yet or not.
@cvar _enumerants: A C{dict} mapping the names of constants (eg
L{NamedConstant} instances) found in the class definition to those
instances. This is initialized via the L{_EnumerantsInitializer}
descriptor the first time it is accessed.
@since: Twisted 12.0.0.
"""
_constantType = None
_enumerantsInitialized = False
_enumerants = _EnumerantsInitializer()
def __new__(cls):
"""
Classes representing constants containers are not intended to be
instantiated.
The class object itself is used directly.
"""
raise TypeError("%s may not be instantiated." % (cls.__name__,))
def _initializeEnumerants(cls):
"""
Find all of the L{NamedConstant} instances in the definition of C{cls},
initialize them with constant values, and build a mapping from their
names to them to attach to C{cls}.
"""
if not cls._enumerantsInitialized:
constants = []
for (name, descriptor) in cls.__dict__.iteritems():
if isinstance(descriptor, cls._constantType):
constants.append((descriptor._index, name, descriptor))
enumerants = {}
for (index, enumerant, descriptor) in constants:
value = cls._constantFactory(enumerant)
descriptor._realize(cls, enumerant, value)
enumerants[enumerant] = descriptor
# Replace the _enumerants descriptor with the result so future
# access will go directly to the values. The _enumerantsInitialized
# flag is still necessary because NamedConstant.__get__ may also
# call this method.
cls._enumerants = enumerants
cls._enumerantsInitialized = True
_initializeEnumerants = classmethod(_initializeEnumerants)
def _constantFactory(cls, name):
"""
Construct the value for a new constant to add to this container.
@param name: The name of the constant to create.
@return: L{NamedConstant} instances have no value apart from identity,
so return a meaningless dummy value.
"""
return _unspecified
_constantFactory = classmethod(_constantFactory)
def lookupByName(cls, name):
"""
Retrieve a constant by its name or raise a C{ValueError} if there is no
constant associated with that name.
@param name: A C{str} giving the name of one of the constants defined by
C{cls}.
@raise ValueError: If C{name} is not the name of one of the constants
defined by C{cls}.
@return: The L{NamedConstant} associated with C{name}.
"""
if name in cls._enumerants:
return getattr(cls, name)
raise ValueError(name)
lookupByName = classmethod(lookupByName)
def iterconstants(cls):
"""
Iteration over a L{Names} subclass results in all of the constants it
contains.
@return: an iterator the elements of which are the L{NamedConstant}
instances defined in the body of this L{Names} subclass.
"""
constants = cls._enumerants.values()
constants.sort(key=lambda descriptor: descriptor._index)
return iter(constants)
iterconstants = classmethod(iterconstants)
class NamedConstant(_Constant):
"""
L{NamedConstant} defines an attribute to be a named constant within a
collection defined by a L{Names} subclass.
L{NamedConstant} is only for use in the definition of L{Names}
subclasses. Do not instantiate L{NamedConstant} elsewhere and do not
subclass it.
@since: Twisted 12.0.0.
"""
class Names(_ConstantsContainer):
"""
A L{Names} subclass contains constants which differ only in their names and
identities.
@since: Twisted 12.0.0.
"""
_constantType = NamedConstant
class ValueConstant(_Constant):
"""
L{ValueConstant} defines an attribute to be a named constant within a
collection defined by a L{Values} subclass.
L{ValueConstant} is only for use in the definition of L{Values} subclasses.
Do not instantiate L{ValueConstant} elsewhere and do not subclass it.
@since: Twisted 12.0.0.
"""
def __init__(self, value):
_Constant.__init__(self)
self.value = value
class Values(_ConstantsContainer):
"""
A L{Values} subclass contains constants which are associated with arbitrary
values.
@since: Twisted 12.0.0.
"""
_constantType = ValueConstant
def lookupByValue(cls, value):
"""
Retrieve a constant by its value or raise a C{ValueError} if there is no
constant associated with that value.
@param value: The value of one of the constants defined by C{cls}.
@raise ValueError: If C{value} is not the value of one of the constants
defined by C{cls}.
@return: The L{ValueConstant} associated with C{value}.
"""
for constant in cls.iterconstants():
if constant.value == value:
return constant
raise ValueError(value)
lookupByValue = classmethod(lookupByValue)
wokkel-0.7.1/wokkel/test/ 0000775 0001750 0001750 00000000000 12074346436 016071 5 ustar ralphm ralphm 0000000 0000000 wokkel-0.7.1/wokkel/test/test_shim.py 0000775 0001750 0001750 00000006131 11707215356 020444 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_shim -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for {wokkel.shim}.
"""
from twisted.trial import unittest
from wokkel import shim
from wokkel.generic import parseXml
NS_SHIM = 'http://jabber.org/protocol/shim'
class HeadersTest(unittest.TestCase):
"""
Tests for L{wokkel.shim.headers}.
"""
def test_noHeaders(self):
headers = shim.Headers([])
self.assertEquals(NS_SHIM, headers.uri)
self.assertEquals('headers', headers.name)
self.assertEquals([], headers.children)
def test_header(self):
headers = shim.Headers([('Urgency', 'high')])
elements = list(headers.elements())
self.assertEquals(1, len(elements))
header = elements[0]
self.assertEquals(NS_SHIM, header.uri)
self.assertEquals('header', header.name)
self.assertEquals('Urgency', header['name'])
self.assertEquals('high', unicode(header))
def test_headerRepeated(self):
"""
Some headers can appear more than once with the same name.
"""
headers = shim.Headers([('Collection', 'node1'),
('Collection', 'node2')])
elements = list(headers.elements())
self.assertEquals(2, len(elements))
collections = set((unicode(element) for element in elements
if element['name'] == 'Collection'))
self.assertIn('node1', collections)
self.assertIn('node2', collections)
class ExtractHeadersTest(unittest.TestCase):
"""
Tests for L{wokkel.shim.extractHeaders}.
"""
def test_noHeaders(self):
"""
A stanza without headers results in an empty dictionary.
"""
stanza = parseXml("""""")
headers = shim.extractHeaders(stanza)
self.assertEquals({}, headers)
def test_headers(self):
"""
A stanza with headers results in a dictionary with those headers.
"""
xml = """
"""
stanza = parseXml(xml)
headers = shim.extractHeaders(stanza)
self.assertEquals({'Urgency': ['high'],
'Collection': ['node1']}, headers)
def test_headersRepeated(self):
"""
Some headers may appear repeatedly. Make sure all values are extracted.
"""
xml = """
"""
stanza = parseXml(xml)
headers = shim.extractHeaders(stanza)
self.assertEquals({'Urgency': ['high'],
'Collection': ['node1', 'node2']}, headers)
wokkel-0.7.1/wokkel/test/test_server.py 0000775 0001750 0001750 00000035755 11707215356 021030 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.server}.
"""
from twisted.internet import defer
from twisted.python import failure
from twisted.test.proto_helpers import StringTransport
from twisted.trial import unittest
from twisted.words.protocols.jabber import error, jid, xmlstream
from twisted.words.xish import domish
from wokkel import component, server
NS_STREAMS = 'http://etherx.jabber.org/streams'
NS_DIALBACK = "jabber:server:dialback"
class GenerateKeyTest(unittest.TestCase):
"""
Tests for L{server.generateKey}.
"""
def testBasic(self):
originating = "example.org"
receiving = "xmpp.example.com"
sid = "D60000229F"
secret = "s3cr3tf0rd14lb4ck"
key = server.generateKey(secret, receiving, originating, sid)
self.assertEqual(key,
'37c69b1cf07a3f67c04a5ef5902fa5114f2c76fe4a2686482ba5b89323075643')
class XMPPServerListenAuthenticatorTest(unittest.TestCase):
"""
Tests for L{server.XMPPServerListenAuthenticator}.
"""
secret = "s3cr3tf0rd14lb4ck"
originating = "example.org"
receiving = "xmpp.example.com"
sid = "D60000229F"
key = '37c69b1cf07a3f67c04a5ef5902fa5114f2c76fe4a2686482ba5b89323075643'
def setUp(self):
self.output = []
class MyService(object):
pass
self.service = MyService()
self.service.defaultDomain = self.receiving
self.service.domains = [self.receiving, 'pubsub.'+self.receiving]
self.service.secret = self.secret
self.authenticator = server.XMPPServerListenAuthenticator(self.service)
self.xmlstream = xmlstream.XmlStream(self.authenticator)
self.xmlstream.send = self.output.append
self.xmlstream.transport = StringTransport()
def test_attributes(self):
"""
Test attributes of authenticator and stream objects.
"""
self.assertEqual(self.service, self.authenticator.service)
self.assertEqual(self.xmlstream.initiating, False)
def test_streamStartedVersion0(self):
"""
The authenticator supports pre-XMPP 1.0 streams.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual((0, 0), self.xmlstream.version)
def test_streamStartedVersion1(self):
"""
The authenticator supports XMPP 1.0 streams.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual((1, 0), self.xmlstream.version)
def test_streamStartedSID(self):
"""
The response stream will have a stream ID.
"""
self.xmlstream.connectionMade()
self.assertIdentical(None, self.xmlstream.sid)
self.xmlstream.dataReceived(
"")
self.assertNotIdentical(None, self.xmlstream.sid)
def test_streamStartedSentResponseHeader(self):
"""
A stream header is sent in response to the incoming stream header.
"""
self.xmlstream.connectionMade()
self.assertFalse(self.xmlstream._headerSent)
self.xmlstream.dataReceived(
"")
self.assertTrue(self.xmlstream._headerSent)
def test_streamStartedNotSentFeatures(self):
"""
No features are sent in response to an XMPP < 1.0 stream header.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(1, len(self.output))
def test_streamStartedSentFeatures(self):
"""
Features are sent in response to an XMPP >= 1.0 stream header.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(2, len(self.output))
features = self.output[-1]
self.assertEqual(NS_STREAMS, features.uri)
self.assertEqual('features', features.name)
def test_streamRootElement(self):
"""
Test stream error on wrong stream namespace.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(3, len(self.output))
exc = error.exceptionFromStreamError(self.output[1])
self.assertEqual('invalid-namespace', exc.condition)
def test_streamDefaultNamespace(self):
"""
Test stream error on missing dialback namespace.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(3, len(self.output))
exc = error.exceptionFromStreamError(self.output[1])
self.assertEqual('invalid-namespace', exc.condition)
def test_streamNoDialbackNamespace(self):
"""
Test stream error on missing dialback namespace.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(3, len(self.output))
exc = error.exceptionFromStreamError(self.output[1])
self.assertEqual('invalid-namespace', exc.condition)
def test_streamBadDialbackNamespace(self):
"""
Test stream error on missing dialback namespace.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(3, len(self.output))
exc = error.exceptionFromStreamError(self.output[1])
self.assertEqual('invalid-namespace', exc.condition)
def test_streamToUnknownHost(self):
"""
Test stream error on stream's to attribute having unknown host.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(3, len(self.output))
exc = error.exceptionFromStreamError(self.output[1])
self.assertEqual('host-unknown', exc.condition)
def test_streamToOtherLocalHost(self):
"""
The authenticator supports XMPP 1.0 streams.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.assertEqual(2, len(self.output))
self.assertEqual(jid.JID('pubsub.xmpp.example.com'),
self.xmlstream.thisEntity)
def test_onResult(self):
def cb(result):
self.assertEqual(1, len(self.output))
reply = self.output[0]
self.assertEqual(self.originating, reply['to'])
self.assertEqual(self.receiving, reply['from'])
self.assertEqual('valid', reply['type'])
def validateConnection(thisHost, otherHost, sid, key):
self.assertEqual(thisHost, self.receiving)
self.assertEqual(otherHost, self.originating)
self.assertEqual(sid, self.sid)
self.assertEqual(key, self.key)
return defer.succeed(None)
self.xmlstream.sid = self.sid
self.service.validateConnection = validateConnection
result = domish.Element((NS_DIALBACK, 'result'))
result['to'] = self.receiving
result['from'] = self.originating
result.addContent(self.key)
d = self.authenticator.onResult(result)
d.addCallback(cb)
return d
def test_onResultFailure(self):
class TestError(Exception):
pass
def cb(result):
reply = self.output[0]
self.assertEqual('invalid', reply['type'])
self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
def validateConnection(thisHost, otherHost, sid, key):
return defer.fail(TestError())
self.xmlstream.sid = self.sid
self.service.validateConnection = validateConnection
result = domish.Element((NS_DIALBACK, 'result'))
result['to'] = self.receiving
result['from'] = self.originating
result.addContent(self.key)
d = self.authenticator.onResult(result)
d.addCallback(cb)
return d
class FakeService(object):
domains = set(['example.org', 'pubsub.example.org'])
defaultDomain = 'example.org'
secret = 'mysecret'
def __init__(self):
self.dispatched = []
def dispatch(self, xs, element):
self.dispatched.append(element)
class XMPPS2SServerFactoryTest(unittest.TestCase):
"""
Tests for L{component.XMPPS2SServerFactory}.
"""
def setUp(self):
self.service = FakeService()
self.factory = server.XMPPS2SServerFactory(self.service)
self.xmlstream = self.factory.buildProtocol(None)
self.transport = StringTransport()
self.xmlstream.thisEntity = jid.JID('example.org')
self.xmlstream.otherEntity = jid.JID('example.com')
def test_makeConnection(self):
"""
A new connection increases the stream serial count. No logs by default.
"""
self.xmlstream.makeConnection(self.transport)
self.assertEqual(0, self.xmlstream.serial)
self.assertEqual(1, self.factory.serial)
self.assertIdentical(None, self.xmlstream.rawDataInFn)
self.assertIdentical(None, self.xmlstream.rawDataOutFn)
def test_makeConnectionLogTraffic(self):
"""
Setting logTraffic should set up raw data loggers.
"""
self.factory.logTraffic = True
self.xmlstream.makeConnection(self.transport)
self.assertNotIdentical(None, self.xmlstream.rawDataInFn)
self.assertNotIdentical(None, self.xmlstream.rawDataOutFn)
def test_onError(self):
"""
An observer for stream errors should trigger onError to log it.
"""
self.xmlstream.makeConnection(self.transport)
class TestError(Exception):
pass
reason = failure.Failure(TestError())
self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT)
self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
def test_connectionInitialized(self):
"""
"""
self.xmlstream.makeConnection(self.transport)
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
def test_connectionLost(self):
"""
"""
self.xmlstream.makeConnection(self.transport)
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT)
def test_Element(self):
self.xmlstream.makeConnection(self.transport)
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
stanza = domish.Element((None, "presence"))
self.xmlstream.dispatch(stanza)
self.assertEqual(1, len(self.service.dispatched))
self.assertIdentical(stanza, self.service.dispatched[-1])
def test_ElementNotAuthenticated(self):
self.xmlstream.makeConnection(self.transport)
stanza = domish.Element((None, "presence"))
self.xmlstream.dispatch(stanza)
self.assertEqual(0, len(self.service.dispatched))
class ServerServiceTest(unittest.TestCase):
def setUp(self):
self.output = []
self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator())
self.xmlstream.thisEntity = jid.JID('example.org')
self.xmlstream.otherEntity = jid.JID('example.com')
self.xmlstream.send = self.output.append
self.router = component.Router()
self.service = server.ServerService(self.router,
secret='mysecret',
domain='example.org')
self.service.xmlstream = self.xmlstream
def test_defaultDomainInDomains(self):
"""
The default domain is part of the domains considered local.
"""
self.assertIn(self.service.defaultDomain, self.service.domains)
def test_dispatch(self):
stanza = domish.Element((None, "presence"))
stanza['to'] = 'user@example.org'
stanza['from'] = 'other@example.com'
self.service.dispatch(self.xmlstream, stanza)
self.assertEqual(1, len(self.output))
self.assertIdentical(stanza, self.output[-1])
def test_dispatchNoTo(self):
errors = []
self.xmlstream.sendStreamError = errors.append
stanza = domish.Element((None, "presence"))
stanza['from'] = 'other@example.com'
self.service.dispatch(self.xmlstream, stanza)
self.assertEqual(1, len(errors))
wokkel-0.7.1/wokkel/test/test_compat.py 0000775 0001750 0001750 00000037317 11707274572 021006 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Twisted Matrix Laboratories.
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.compat}.
"""
from zope.interface import implements
from twisted.internet import task
from twisted.internet.interfaces import IReactorTime
from twisted.trial import unittest
from twisted.words.protocols.jabber import xmlstream
from wokkel.compat import IQ
from wokkel.compat import NamedConstant, Names, ValueConstant, Values
class DeprecationTest(unittest.TestCase):
"""
Deprecation tests for L{wokkel.compat}.
"""
def lookForDeprecationWarning(self, testmethod, attributeName, newName):
"""
Importing C{testmethod} emits a deprecation warning.
"""
warningsShown = self.flushWarnings([testmethod])
self.assertEqual(len(warningsShown), 1)
self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
self.assertEqual(
warningsShown[0]['message'],
"wokkel.compat." + attributeName + " "
"was deprecated in Wokkel 0.7.0: Use " + newName + " instead.")
def test_bootstrapMixinTest(self):
"""
L{compat.BootstrapMixin} is deprecated.
"""
from wokkel.compat import BootstrapMixin
BootstrapMixin
self.lookForDeprecationWarning(
self.test_bootstrapMixinTest,
"BootstrapMixin",
"twisted.words.xish.xmlstream.BootstrapMixin")
def test_xmlStreamServerFactory(self):
"""
L{compat.XmlStreamServerFactory} is deprecated.
"""
from wokkel.compat import XmlStreamServerFactory
XmlStreamServerFactory
self.lookForDeprecationWarning(
self.test_xmlStreamServerFactory,
"XmlStreamServerFactory",
"twisted.words.protocols.jabber.xmlstream."
"XmlStreamServerFactory")
class FakeReactor(object):
implements(IReactorTime)
def __init__(self):
self.clock = task.Clock()
self.callLater = self.clock.callLater
self.getDelayedCalls = self.clock.getDelayedCalls
class IQTest(unittest.TestCase):
"""
Tests for L{IQ}.
"""
def setUp(self):
self.reactor = FakeReactor()
self.clock = self.reactor.clock
def testRequestTimingOutEventDispatcher(self):
"""
Test that an iq request with a defined timeout times out.
"""
from twisted.words.xish import utility
output = []
xs = utility.EventDispatcher()
xs.send = output.append
self.iq = IQ(xs, reactor=self.reactor)
self.iq.timeout = 60
d = self.iq.send()
self.assertFailure(d, xmlstream.TimeoutError)
self.clock.pump([1, 60])
self.assertFalse(self.reactor.getDelayedCalls())
self.assertFalse(xs.iqDeferreds)
return d
class NamedConstantTests(unittest.TestCase):
"""
Tests for the L{twisted.python.constants.NamedConstant} class which is used
to represent individual values.
"""
def setUp(self):
"""
Create a dummy container into which constants can be placed.
"""
class foo(Names):
pass
self.container = foo
def test_name(self):
"""
The C{name} attribute of a L{NamedConstant} refers to the value passed
for the C{name} parameter to C{_realize}.
"""
name = NamedConstant()
name._realize(self.container, "bar", None)
self.assertEqual("bar", name.name)
def test_representation(self):
"""
The string representation of an instance of L{NamedConstant} includes
the container the instances belongs to as well as the instance's name.
"""
name = NamedConstant()
name._realize(self.container, "bar", None)
self.assertEqual("", repr(name))
def test_equality(self):
"""
A L{NamedConstant} instance compares equal to itself.
"""
name = NamedConstant()
name._realize(self.container, "bar", None)
self.assertTrue(name == name)
self.assertFalse(name != name)
def test_nonequality(self):
"""
Two different L{NamedConstant} instances do not compare equal to each
other.
"""
first = NamedConstant()
first._realize(self.container, "bar", None)
second = NamedConstant()
second._realize(self.container, "bar", None)
self.assertFalse(first == second)
self.assertTrue(first != second)
def test_hash(self):
"""
Because two different L{NamedConstant} instances do not compare as equal
to each other, they also have different hashes to avoid collisions when
added to a C{dict} or C{set}.
"""
first = NamedConstant()
first._realize(self.container, "bar", None)
second = NamedConstant()
second._realize(self.container, "bar", None)
self.assertNotEqual(hash(first), hash(second))
class _ConstantsTestsMixin(object):
"""
Mixin defining test helpers common to multiple types of constants
collections.
"""
def _notInstantiableTest(self, name, cls):
"""
Assert that an attempt to instantiate the constants class raises
C{TypeError}.
@param name: A C{str} giving the name of the constants collection.
@param cls: The constants class to test.
"""
exc = self.assertRaises(TypeError, cls)
self.assertEqual(name + " may not be instantiated.", str(exc))
class NamesTests(unittest.TestCase, _ConstantsTestsMixin):
"""
Tests for L{twisted.python.constants.Names}, a base class for containers of
related constaints.
"""
def setUp(self):
"""
Create a fresh new L{Names} subclass for each unit test to use. Since
L{Names} is stateful, re-using the same subclass across test methods
makes exercising all of the implementation code paths difficult.
"""
class METHOD(Names):
"""
A container for some named constants to use in unit tests for
L{Names}.
"""
GET = NamedConstant()
PUT = NamedConstant()
POST = NamedConstant()
DELETE = NamedConstant()
self.METHOD = METHOD
def test_notInstantiable(self):
"""
A subclass of L{Names} raises C{TypeError} if an attempt is made to
instantiate it.
"""
self._notInstantiableTest("METHOD", self.METHOD)
def test_symbolicAttributes(self):
"""
Each name associated with a L{NamedConstant} instance in the definition
of a L{Names} subclass is available as an attribute on the resulting
class.
"""
self.assertTrue(hasattr(self.METHOD, "GET"))
self.assertTrue(hasattr(self.METHOD, "PUT"))
self.assertTrue(hasattr(self.METHOD, "POST"))
self.assertTrue(hasattr(self.METHOD, "DELETE"))
def test_withoutOtherAttributes(self):
"""
As usual, names not defined in the class scope of a L{Names}
subclass are not available as attributes on the resulting class.
"""
self.assertFalse(hasattr(self.METHOD, "foo"))
def test_representation(self):
"""
The string representation of a constant on a L{Names} subclass includes
the name of the L{Names} subclass and the name of the constant itself.
"""
self.assertEqual("", repr(self.METHOD.GET))
def test_lookupByName(self):
"""
Constants can be looked up by name using L{Names.lookupByName}.
"""
method = self.METHOD.lookupByName("GET")
self.assertIdentical(self.METHOD.GET, method)
def test_notLookupMissingByName(self):
"""
Names not defined with a L{NamedConstant} instance cannot be looked up
using L{Names.lookupByName}.
"""
self.assertRaises(ValueError, self.METHOD.lookupByName, "lookupByName")
self.assertRaises(ValueError, self.METHOD.lookupByName, "__init__")
self.assertRaises(ValueError, self.METHOD.lookupByName, "foo")
def test_name(self):
"""
The C{name} attribute of one of the named constants gives that
constant's name.
"""
self.assertEqual("GET", self.METHOD.GET.name)
def test_attributeIdentity(self):
"""
Repeated access of an attribute associated with a L{NamedConstant} value
in a L{Names} subclass results in the same object.
"""
self.assertIdentical(self.METHOD.GET, self.METHOD.GET)
def test_iterconstants(self):
"""
L{Names.iterconstants} returns an iterator over all of the constants
defined in the class, in the order they were defined.
"""
constants = list(self.METHOD.iterconstants())
self.assertEqual(
[self.METHOD.GET, self.METHOD.PUT,
self.METHOD.POST, self.METHOD.DELETE],
constants)
def test_attributeIterconstantsIdentity(self):
"""
The constants returned from L{Names.iterconstants} are identical to the
constants accessible using attributes.
"""
constants = list(self.METHOD.iterconstants())
self.assertIdentical(self.METHOD.GET, constants[0])
self.assertIdentical(self.METHOD.PUT, constants[1])
self.assertIdentical(self.METHOD.POST, constants[2])
self.assertIdentical(self.METHOD.DELETE, constants[3])
def test_iterconstantsIdentity(self):
"""
The constants returned from L{Names.iterconstants} are identical on each
call to that method.
"""
constants = list(self.METHOD.iterconstants())
again = list(self.METHOD.iterconstants())
self.assertIdentical(again[0], constants[0])
self.assertIdentical(again[1], constants[1])
self.assertIdentical(again[2], constants[2])
self.assertIdentical(again[3], constants[3])
def test_initializedOnce(self):
"""
L{Names._enumerants} is initialized once and its value re-used on
subsequent access.
"""
first = self.METHOD._enumerants
self.METHOD.GET # Side-effects!
second = self.METHOD._enumerants
self.assertIdentical(first, second)
class ValuesTests(unittest.TestCase, _ConstantsTestsMixin):
"""
Tests for L{twisted.python.constants.Names}, a base class for containers of
related constaints with arbitrary values.
"""
def setUp(self):
"""
Create a fresh new L{Values} subclass for each unit test to use. Since
L{Values} is stateful, re-using the same subclass across test methods
makes exercising all of the implementation code paths difficult.
"""
class STATUS(Values):
OK = ValueConstant("200")
NOT_FOUND = ValueConstant("404")
self.STATUS = STATUS
def test_notInstantiable(self):
"""
A subclass of L{Values} raises C{TypeError} if an attempt is made to
instantiate it.
"""
self._notInstantiableTest("STATUS", self.STATUS)
def test_symbolicAttributes(self):
"""
Each name associated with a L{ValueConstant} instance in the definition
of a L{Values} subclass is available as an attribute on the resulting
class.
"""
self.assertTrue(hasattr(self.STATUS, "OK"))
self.assertTrue(hasattr(self.STATUS, "NOT_FOUND"))
def test_withoutOtherAttributes(self):
"""
As usual, names not defined in the class scope of a L{Values}
subclass are not available as attributes on the resulting class.
"""
self.assertFalse(hasattr(self.STATUS, "foo"))
def test_representation(self):
"""
The string representation of a constant on a L{Values} subclass includes
the name of the L{Values} subclass and the name of the constant itself.
"""
self.assertEqual("", repr(self.STATUS.OK))
def test_lookupByName(self):
"""
Constants can be looked up by name using L{Values.lookupByName}.
"""
method = self.STATUS.lookupByName("OK")
self.assertIdentical(self.STATUS.OK, method)
def test_notLookupMissingByName(self):
"""
Names not defined with a L{ValueConstant} instance cannot be looked up
using L{Values.lookupByName}.
"""
self.assertRaises(ValueError, self.STATUS.lookupByName, "lookupByName")
self.assertRaises(ValueError, self.STATUS.lookupByName, "__init__")
self.assertRaises(ValueError, self.STATUS.lookupByName, "foo")
def test_lookupByValue(self):
"""
Constants can be looked up by their associated value, defined by the
argument passed to L{ValueConstant}, using L{Values.lookupByValue}.
"""
status = self.STATUS.lookupByValue("200")
self.assertIdentical(self.STATUS.OK, status)
def test_lookupDuplicateByValue(self):
"""
If more than one constant is associated with a particular value,
L{Values.lookupByValue} returns whichever of them is defined first.
"""
class TRANSPORT_MESSAGE(Values):
"""
Message types supported by an SSH transport.
"""
KEX_DH_GEX_REQUEST_OLD = ValueConstant(30)
KEXDH_INIT = ValueConstant(30)
self.assertIdentical(
TRANSPORT_MESSAGE.lookupByValue(30),
TRANSPORT_MESSAGE.KEX_DH_GEX_REQUEST_OLD)
def test_notLookupMissingByValue(self):
"""
L{Values.lookupByValue} raises L{ValueError} when called with a value
with which no constant is associated.
"""
self.assertRaises(ValueError, self.STATUS.lookupByValue, "OK")
self.assertRaises(ValueError, self.STATUS.lookupByValue, 200)
self.assertRaises(ValueError, self.STATUS.lookupByValue, "200.1")
def test_name(self):
"""
The C{name} attribute of one of the constants gives that constant's
name.
"""
self.assertEqual("OK", self.STATUS.OK.name)
def test_attributeIdentity(self):
"""
Repeated access of an attribute associated with a L{ValueConstant} value
in a L{Values} subclass results in the same object.
"""
self.assertIdentical(self.STATUS.OK, self.STATUS.OK)
def test_iterconstants(self):
"""
L{Values.iterconstants} returns an iterator over all of the constants
defined in the class, in the order they were defined.
"""
constants = list(self.STATUS.iterconstants())
self.assertEqual(
[self.STATUS.OK, self.STATUS.NOT_FOUND],
constants)
def test_attributeIterconstantsIdentity(self):
"""
The constants returned from L{Values.iterconstants} are identical to the
constants accessible using attributes.
"""
constants = list(self.STATUS.iterconstants())
self.assertIdentical(self.STATUS.OK, constants[0])
self.assertIdentical(self.STATUS.NOT_FOUND, constants[1])
def test_iterconstantsIdentity(self):
"""
The constants returned from L{Values.iterconstants} are identical on
each call to that method.
"""
constants = list(self.STATUS.iterconstants())
again = list(self.STATUS.iterconstants())
self.assertIdentical(again[0], constants[0])
self.assertIdentical(again[1], constants[1])
def test_initializedOnce(self):
"""
L{Values._enumerants} is initialized once and its value re-used on
subsequent access.
"""
first = self.STATUS._enumerants
self.STATUS.OK # Side-effects!
second = self.STATUS._enumerants
self.assertIdentical(first, second)
wokkel-0.7.1/wokkel/test/helpers.py 0000775 0001750 0001750 00000007170 12074262111 020100 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Unit test helpers.
"""
from twisted.internet import defer
from twisted.words.xish import xpath
from twisted.words.xish.utility import EventDispatcher
from wokkel.generic import parseXml
from wokkel.subprotocols import StreamManager
class XmlStreamStub(object):
"""
Stub for testing objects that communicate through XML Streams.
Instances of this stub hold an object in L{xmlstream} that acts like an
L{XmlStream} as its first argument.
These appear in sequence in the L{output} instance variable of the stub.
For the reverse direction, stanzas passed to L{send} of the stub, will be
dispatched in the stubbed XmlStream as if it was received over the wire, so
that registered observers will be called.
Example:
>>> stub = XmlStreamStub()
>>> stub.xmlstream.send(domish.Element((None, 'presence')))
>>> stub.output[-1].toXml()
u''
>>> def cb(stanza):
... print "Got: %r" stanza.toXml()
>>> stub.xmlstream.addObserver('/presence')
>>> stub.send(domish.Element((None, 'presence')))
Got: u''
@ivar xmlstream: Stubbed XML Stream.
@type xmlstream: L{EventDispatcher}
@ivar output: List of stanzas sent to the XML Stream.
@type output: L{list}
"""
def __init__(self):
self.output = []
self.xmlstream = EventDispatcher()
self.xmlstream.send = self.output.append
def send(self, obj):
"""
Pass an element to the XML Stream as if received.
@param obj: Element to be dispatched to C{self.xmlstream}.
@type obj: object implementing
L{IElement}.
"""
self.xmlstream.dispatch(obj)
class TestableRequestHandlerMixin(object):
"""
Mixin for testing XMPPHandlers that process iq requests.
Handlers that use L{wokkel.subprotocols.IQHandlerMixin} define a
C{iqHandlers} attribute that lists the handlers to be called for iq
requests. This mixin provides L{handleRequest} to mimic the handler
processing for easier testing.
"""
def handleRequest(self, xml):
"""
Find a handler and call it directly.
@param xml: XML stanza that may yield a handler being called.
@type xml: C{str}.
@return: Deferred that fires with the result of a handler for this
stanza. If no handler was found, the deferred has its errback
called with a C{NotImplementedError} exception.
"""
handler = None
iq = parseXml(xml)
for queryString, method in self.service.iqHandlers.iteritems():
if xpath.internQuery(queryString).matches(iq):
handler = getattr(self.service, method)
if handler:
d = defer.maybeDeferred(handler, iq)
else:
d = defer.fail(NotImplementedError())
return d
class TestableStreamManager(StreamManager):
"""
Stream manager for testing subprotocol handlers.
"""
def __init__(self, reactor=None):
class DummyFactory(object):
def addBootstrap(self, event, fn):
pass
factory = DummyFactory()
StreamManager.__init__(self, factory, reactor)
self.stub = XmlStreamStub()
self._connected(self.stub.xmlstream)
self._authd(self.stub.xmlstream)
wokkel-0.7.1/wokkel/test/test_component.py 0000775 0001750 0001750 00000040764 11722233042 021505 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.component}.
"""
from zope.interface.verify import verifyObject
from twisted.internet.base import BaseConnector
from twisted.internet.error import ConnectionRefusedError
from twisted.internet.task import Clock
from twisted.python import failure
from twisted.trial import unittest
from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber.ijabber import IXMPPHandlerCollection
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from wokkel import component
from wokkel.generic import XmlPipe
class FakeConnector(BaseConnector):
"""
Fake connector that counts connection attempts.
"""
connects = 0
def connect(self):
self.connects += 1
BaseConnector.connect(self)
def _makeTransport(self):
return None
class TestableComponent(component.Component):
"""
Testable component.
This component provides the created factory with a L{Clock}
instead of the regular reactor and uses L{FakeConnector} for testing
connects and reconnects.
"""
def __init__(self, *args, **kwargs):
component.Component.__init__(self, *args, **kwargs)
self.factory.clock = Clock()
def _getConnection(self):
c = FakeConnector(self.factory, None, None)
c.connect()
return c
class ComponentTest(unittest.TestCase):
"""
Tests for L{component.Component}.
"""
def test_startServiceReconnectAfterFailure(self):
"""
When the first connection attempt fails, retry.
"""
comp = TestableComponent('example.org', 5347,
'test.example.org', 'secret')
# Starting the service initiates a connection attempt.
comp.startService()
connector = comp._connection
self.assertEqual(1, connector.connects)
# Fail the connection.
connector.connectionFailed(ConnectionRefusedError())
# After a back-off delay, a new connection is attempted.
comp.factory.clock.advance(5)
self.assertEqual(2, connector.connects)
def test_stopServiceNoReconnect(self):
"""
When the service is stopped, no reconnect is attempted.
"""
comp = TestableComponent('example.org', 5347,
'test.example.org', 'secret')
# Starting the service initiates a connection attempt.
comp.startService()
connector = comp._connection
# Fail the connection.
connector.connectionFailed(ConnectionRefusedError())
# If the service is stopped before the back-off delay expires,
# no new connection is attempted.
comp.factory.clock.advance(1)
comp.stopService()
comp.factory.clock.advance(4)
self.assertEqual(1, connector.connects)
class InternalComponentTest(unittest.TestCase):
"""
Tests for L{component.InternalComponent}.
"""
def setUp(self):
self.router = component.Router()
self.component = component.InternalComponent(self.router, 'component')
def test_interface(self):
"""
L{component.InternalComponent} implements
L{IXMPPHandlerCollection}.
"""
verifyObject(IXMPPHandlerCollection, self.component)
def test_startServiceRunning(self):
"""
Starting the service makes it running.
"""
self.assertFalse(self.component.running)
self.component.startService()
self.assertTrue(self.component.running)
def test_startServiceAddRoute(self):
"""
Starting the service creates a new route.
"""
self.component.startService()
self.assertIn('component', self.router.routes)
def test_startServiceNoDomain(self):
self.component = component.InternalComponent(self.router)
self.component.startService()
def test_startServiceAddMultipleRoutes(self):
"""
Starting the service creates a new route.
"""
self.component.domains.add('component2')
self.component.startService()
self.assertIn('component', self.router.routes)
self.assertIn('component2', self.router.routes)
def test_startServiceHandlerDispatch(self):
"""
Starting the service hooks up handlers.
"""
events = []
class TestHandler(XMPPHandler):
def connectionInitialized(self):
fn = lambda obj: events.append(obj)
self.xmlstream.addObserver('//event/test', fn)
TestHandler().setHandlerParent(self.component)
self.component.startService()
self.assertEquals([], events)
self.component.xmlstream.dispatch(None, '//event/test')
self.assertEquals([None], events)
def test_stopServiceNotRunning(self):
"""
Stopping the service makes it not running.
"""
self.component.startService()
self.component.stopService()
self.assertFalse(self.component.running)
def test_stopServiceRemoveRoute(self):
"""
Stopping the service removes routes.
"""
self.component.startService()
self.component.stopService()
self.assertNotIn('component', self.router.routes)
def test_stopServiceNoDomain(self):
self.component = component.InternalComponent(self.router)
self.component.startService()
self.component.stopService()
def test_startServiceRemoveMultipleRoutes(self):
"""
Starting the service creates a new route.
"""
self.component.domains.add('component2')
self.component.startService()
self.component.stopService()
self.assertNotIn('component', self.router.routes)
self.assertNotIn('component2', self.router.routes)
def test_stopServiceHandlerDispatch(self):
"""
Stopping the service disconnects handlers.
"""
events = []
class TestHandler(XMPPHandler):
def connectionLost(self, reason):
events.append(reason)
TestHandler().setHandlerParent(self.component)
self.component.startService()
self.component.stopService()
self.assertEquals(1, len(events))
def test_addHandler(self):
"""
Adding a handler connects it to the stream.
"""
events = []
class TestHandler(XMPPHandler):
def connectionInitialized(self):
fn = lambda obj: events.append(obj)
self.xmlstream.addObserver('//event/test', fn)
self.component.startService()
self.component.xmlstream.dispatch(None, '//event/test')
self.assertEquals([], events)
TestHandler().setHandlerParent(self.component)
self.component.xmlstream.dispatch(None, '//event/test')
self.assertEquals([None], events)
def test_send(self):
"""
A message sent from the component ends up at the router.
"""
events = []
fn = lambda obj: events.append(obj)
message = domish.Element((None, 'message'))
self.router.route = fn
self.component.startService()
self.component.send(message)
self.assertEquals([message], events)
class RouterTest(unittest.TestCase):
"""
Tests for L{component.Router}.
"""
def test_addRoute(self):
"""
Test route registration and routing on incoming stanzas.
"""
router = component.Router()
routed = []
router.route = lambda element: routed.append(element)
pipe = XmlPipe()
router.addRoute('example.org', pipe.sink)
self.assertEquals(1, len(router.routes))
self.assertEquals(pipe.sink, router.routes['example.org'])
element = domish.Element(('testns', 'test'))
pipe.source.send(element)
self.assertEquals([element], routed)
def test_route(self):
"""
Test routing of a message.
"""
component1 = XmlPipe()
component2 = XmlPipe()
router = component.Router()
router.addRoute('component1.example.org', component1.sink)
router.addRoute('component2.example.org', component2.sink)
outgoing = []
component2.source.addObserver('/*',
lambda element: outgoing.append(element))
stanza = domish.Element((None, 'presence'))
stanza['from'] = 'component1.example.org'
stanza['to'] = 'component2.example.org'
component1.source.send(stanza)
self.assertEquals([stanza], outgoing)
def test_routeDefault(self):
"""
Test routing of a message using the default route.
The default route is the one with C{None} as its key in the
routing table. It is taken when there is no more specific route
in the routing table that matches the stanza's destination.
"""
component1 = XmlPipe()
s2s = XmlPipe()
router = component.Router()
router.addRoute('component1.example.org', component1.sink)
router.addRoute(None, s2s.sink)
outgoing = []
s2s.source.addObserver('/*', lambda element: outgoing.append(element))
stanza = domish.Element((None, 'presence'))
stanza['from'] = 'component1.example.org'
stanza['to'] = 'example.com'
component1.source.send(stanza)
self.assertEquals([stanza], outgoing)
class ListenComponentAuthenticatorTest(unittest.TestCase):
"""
Tests for L{component.ListenComponentAuthenticator}.
"""
def setUp(self):
self.output = []
authenticator = component.ListenComponentAuthenticator('secret')
self.xmlstream = xmlstream.XmlStream(authenticator)
self.xmlstream.send = self.output.append
def loseConnection(self):
"""
Stub loseConnection because we are a transport.
"""
self.xmlstream.connectionLost("no reason")
def test_streamStarted(self):
"""
The received stream header should set several attributes.
"""
observers = []
def addOnetimeObserver(event, observerfn):
observers.append((event, observerfn))
xs = self.xmlstream
xs.addOnetimeObserver = addOnetimeObserver
xs.makeConnection(self)
self.assertIdentical(None, xs.sid)
self.assertFalse(xs._headerSent)
xs.dataReceived("")
self.assertEqual((0, 0), xs.version)
self.assertNotIdentical(None, xs.sid)
self.assertTrue(xs._headerSent)
self.assertEquals(('/*', xs.authenticator.onElement), observers[-1])
def test_streamStartedWrongNamespace(self):
"""
The received stream header should have a correct namespace.
"""
streamErrors = []
xs = self.xmlstream
xs.sendStreamError = streamErrors.append
xs.makeConnection(self)
xs.dataReceived("")
self.assertEquals(1, len(streamErrors))
self.assertEquals('invalid-namespace', streamErrors[-1].condition)
def test_streamStartedNoTo(self):
"""
The received stream header should have a 'to' attribute.
"""
streamErrors = []
xs = self.xmlstream
xs.sendStreamError = streamErrors.append
xs.makeConnection(self)
xs.dataReceived("")
self.assertEquals(1, len(streamErrors))
self.assertEquals('improper-addressing', streamErrors[-1].condition)
def test_onElement(self):
"""
We expect a handshake element with a hash.
"""
handshakes = []
xs = self.xmlstream
xs.authenticator.onHandshake = handshakes.append
handshake = domish.Element(('jabber:component:accept', 'handshake'))
handshake.addContent('1234')
xs.authenticator.onElement(handshake)
self.assertEqual('1234', handshakes[-1])
def test_onElementNotHandshake(self):
"""
Reject elements that are not handshakes
"""
handshakes = []
streamErrors = []
xs = self.xmlstream
xs.authenticator.onHandshake = handshakes.append
xs.sendStreamError = streamErrors.append
element = domish.Element(('jabber:component:accept', 'message'))
xs.authenticator.onElement(element)
self.assertFalse(handshakes)
self.assertEquals('not-authorized', streamErrors[-1].condition)
def test_onHandshake(self):
"""
Receiving a handshake matching the secret authenticates the stream.
"""
authd = []
def authenticated(xs):
authd.append(xs)
xs = self.xmlstream
xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
xs.sid = u'1234'
theHash = '32532c0f7dbf1253c095b18b18e36d38d94c1256'
xs.authenticator.onHandshake(theHash)
self.assertEqual('', self.output[-1])
self.assertEquals(1, len(authd))
def test_onHandshakeWrongHash(self):
"""
Receiving a bad handshake should yield a stream error.
"""
streamErrors = []
authd = []
def authenticated(xs):
authd.append(xs)
xs = self.xmlstream
xs.addOnetimeObserver(xmlstream.STREAM_AUTHD_EVENT, authenticated)
xs.sendStreamError = streamErrors.append
xs.sid = u'1234'
theHash = '1234'
xs.authenticator.onHandshake(theHash)
self.assertEquals('not-authorized', streamErrors[-1].condition)
self.assertEquals(0, len(authd))
class XMPPComponentServerFactoryTest(unittest.TestCase):
"""
Tests for L{component.XMPPComponentServerFactory}.
"""
def setUp(self):
self.router = component.Router()
self.factory = component.XMPPComponentServerFactory(self.router,
'secret')
self.xmlstream = self.factory.buildProtocol(None)
self.xmlstream.thisEntity = JID('component.example.org')
def test_makeConnection(self):
"""
A new connection increases the stream serial count. No logs by default.
"""
self.xmlstream.dispatch(self.xmlstream,
xmlstream.STREAM_CONNECTED_EVENT)
self.assertEqual(0, self.xmlstream.serial)
self.assertEqual(1, self.factory.serial)
self.assertIdentical(None, self.xmlstream.rawDataInFn)
self.assertIdentical(None, self.xmlstream.rawDataOutFn)
def test_makeConnectionLogTraffic(self):
"""
Setting logTraffic should set up raw data loggers.
"""
self.factory.logTraffic = True
self.xmlstream.dispatch(self.xmlstream,
xmlstream.STREAM_CONNECTED_EVENT)
self.assertNotIdentical(None, self.xmlstream.rawDataInFn)
self.assertNotIdentical(None, self.xmlstream.rawDataOutFn)
def test_onError(self):
"""
An observer for stream errors should trigger onError to log it.
"""
self.xmlstream.dispatch(self.xmlstream,
xmlstream.STREAM_CONNECTED_EVENT)
class TestError(Exception):
pass
reason = failure.Failure(TestError())
self.xmlstream.dispatch(reason, xmlstream.STREAM_ERROR_EVENT)
self.assertEqual(1, len(self.flushLoggedErrors(TestError)))
def test_connectionInitialized(self):
"""
Make sure a new stream is added to the routing table.
"""
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
self.assertIn('component.example.org', self.router.routes)
self.assertIdentical(self.xmlstream,
self.router.routes['component.example.org'])
def test_connectionLost(self):
"""
Make sure a stream is removed from the routing table on disconnect.
"""
self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
self.xmlstream.dispatch(None, xmlstream.STREAM_END_EVENT)
self.assertNotIn('component.example.org', self.router.routes)
wokkel-0.7.1/wokkel/test/test_xmppim.py 0000664 0001750 0001750 00000123650 12074337005 021013 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details
"""
Tests for L{wokkel.xmppim}.
"""
from twisted.internet import defer
from twisted.trial import unittest
from twisted.words.protocols.jabber import error
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import toResponse
from twisted.words.xish import domish, utility
from wokkel import xmppim
from wokkel.generic import ErrorStanza, parseXml
from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
NS_XML = 'http://www.w3.org/XML/1998/namespace'
NS_ROSTER = 'jabber:iq:roster'
class PresenceClientProtocolTest(unittest.TestCase):
def setUp(self):
self.output = []
self.protocol = xmppim.PresenceClientProtocol()
self.protocol.parent = self
def send(self, obj):
self.output.append(obj)
def test_unavailableDirected(self):
"""
Test sending of directed unavailable presence broadcast.
"""
self.protocol.unavailable(JID('user@example.com'))
presence = self.output[-1]
self.assertEquals("presence", presence.name)
self.assertEquals(None, presence.uri)
self.assertEquals("user@example.com", presence.getAttribute('to'))
self.assertEquals("unavailable", presence.getAttribute('type'))
def test_unavailableWithStatus(self):
"""
Test sending of directed unavailable presence broadcast with status.
"""
self.protocol.unavailable(JID('user@example.com'),
{None: 'Disconnected'})
presence = self.output[-1]
self.assertEquals("presence", presence.name)
self.assertEquals(None, presence.uri)
self.assertEquals("user@example.com", presence.getAttribute('to'))
self.assertEquals("unavailable", presence.getAttribute('type'))
self.assertEquals("Disconnected", unicode(presence.status))
def test_unavailableBroadcast(self):
"""
Test sending of unavailable presence broadcast.
"""
self.protocol.unavailable(None)
presence = self.output[-1]
self.assertEquals("presence", presence.name)
self.assertEquals(None, presence.uri)
self.assertEquals(None, presence.getAttribute('to'))
self.assertEquals("unavailable", presence.getAttribute('type'))
def test_unavailableBroadcastNoEntityParameter(self):
"""
Test sending of unavailable presence broadcast by not passing entity.
"""
self.protocol.unavailable()
presence = self.output[-1]
self.assertEquals("presence", presence.name)
self.assertEquals(None, presence.uri)
self.assertEquals(None, presence.getAttribute('to'))
self.assertEquals("unavailable", presence.getAttribute('type'))
class AvailabilityPresenceTest(unittest.TestCase):
def test_fromElement(self):
xml = """
chat
Let's chat!
50
"""
presence = xmppim.AvailabilityPresence.fromElement(parseXml(xml))
self.assertEquals(JID('user@example.org'), presence.sender)
self.assertEquals(JID('user@example.com'), presence.recipient)
self.assertTrue(presence.available)
self.assertEquals('chat', presence.show)
self.assertEquals({None: "Let's chat!"}, presence.statuses)
self.assertEquals(50, presence.priority)
class PresenceProtocolTest(unittest.TestCase):
"""
Tests for L{xmppim.PresenceProtocol}
"""
def setUp(self):
self.output = []
self.protocol = xmppim.PresenceProtocol()
self.protocol.parent = self
self.protocol.xmlstream = utility.EventDispatcher()
self.protocol.connectionInitialized()
def send(self, obj):
self.output.append(obj)
def test_errorReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def errorReceived(error):
xmppim.PresenceProtocol.errorReceived(self.protocol, error)
try:
self.assertIsInstance(error, ErrorStanza)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.errorReceived = errorReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_availableReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def availableReceived(presence):
xmppim.PresenceProtocol.availableReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.AvailabilityPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.availableReceived = availableReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_unavailableReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def unavailableReceived(presence):
xmppim.PresenceProtocol.unavailableReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.AvailabilityPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.unavailableReceived = unavailableReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_subscribeReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def subscribeReceived(presence):
xmppim.PresenceProtocol.subscribeReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.SubscriptionPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.subscribeReceived = subscribeReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_unsubscribeReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def unsubscribeReceived(presence):
xmppim.PresenceProtocol.unsubscribeReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.SubscriptionPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.unsubscribeReceived = unsubscribeReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_subscribedReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def subscribedReceived(presence):
xmppim.PresenceProtocol.subscribedReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.SubscriptionPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.subscribedReceived = subscribedReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_unsubscribedReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def unsubscribedReceived(presence):
xmppim.PresenceProtocol.unsubscribedReceived(self.protocol,
presence)
try:
self.assertIsInstance(presence, xmppim.SubscriptionPresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.unsubscribedReceived = unsubscribedReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_probeReceived(self):
"""
Incoming presence stanzas are parsed and dispatched.
"""
xml = """"""
def probeReceived(presence):
xmppim.PresenceProtocol.probeReceived(self.protocol, presence)
try:
self.assertIsInstance(presence, xmppim.ProbePresence)
except:
d.errback()
else:
d.callback(None)
d = defer.Deferred()
self.protocol.probeReceived = probeReceived
self.protocol.xmlstream.dispatch(parseXml(xml))
return d
def test_available(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.available(JID('user@example.com'),
show=u'chat',
status=u'Talk to me!',
priority=50)
element = self.output[-1]
self.assertEquals("user@example.com", element.getAttribute('to'))
self.assertIdentical(None, element.getAttribute('type'))
self.assertEquals(u'chat', unicode(element.show))
self.assertEquals(u'Talk to me!', unicode(element.status))
self.assertEquals(u'50', unicode(element.priority))
def test_availableLanguages(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.available(JID('user@example.com'),
show=u'chat',
statuses={None: u'Talk to me!',
'nl': u'Praat met me!'},
priority=50)
element = self.output[-1]
self.assertEquals("user@example.com", element.getAttribute('to'))
self.assertIdentical(None, element.getAttribute('type'))
self.assertEquals(u'chat', unicode(element.show))
statuses = {}
for status in element.elements():
if status.name == 'status':
lang = status.getAttribute((NS_XML, 'lang'))
statuses[lang] = unicode(status)
self.assertIn(None, statuses)
self.assertEquals(u'Talk to me!', statuses[None])
self.assertIn('nl', statuses)
self.assertEquals(u'Praat met me!', statuses['nl'])
self.assertEquals(u'50', unicode(element.priority))
def test_availableSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.available(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_unavailableDirected(self):
"""
Test sending of directed unavailable presence broadcast.
"""
self.protocol.unavailable(JID('user@example.com'))
element = self.output[-1]
self.assertEquals("presence", element.name)
self.assertEquals(None, element.uri)
self.assertEquals("user@example.com", element.getAttribute('to'))
self.assertEquals("unavailable", element.getAttribute('type'))
def test_unavailableWithStatus(self):
"""
Test sending of directed unavailable presence broadcast with status.
"""
self.protocol.unavailable(JID('user@example.com'),
{None: 'Disconnected'})
element = self.output[-1]
self.assertEquals("presence", element.name)
self.assertEquals(None, element.uri)
self.assertEquals("user@example.com", element.getAttribute('to'))
self.assertEquals("unavailable", element.getAttribute('type'))
self.assertEquals("Disconnected", unicode(element.status))
def test_unavailableBroadcast(self):
"""
Test sending of unavailable presence broadcast.
"""
self.protocol.unavailable(None)
element = self.output[-1]
self.assertEquals("presence", element.name)
self.assertEquals(None, element.uri)
self.assertEquals(None, element.getAttribute('to'))
self.assertEquals("unavailable", element.getAttribute('type'))
def test_unavailableBroadcastNoRecipientParameter(self):
"""
Test sending of unavailable presence broadcast by not passing entity.
"""
self.protocol.unavailable()
element = self.output[-1]
self.assertEquals("presence", element.name)
self.assertEquals(None, element.uri)
self.assertEquals(None, element.getAttribute('to'))
self.assertEquals("unavailable", element.getAttribute('type'))
def test_unavailableSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.unavailable(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_subscribeSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.subscribe(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_unsubscribeSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.unsubscribe(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_subscribedSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.subscribed(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_unsubscribedSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.unsubscribed(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
def test_probeSender(self):
"""
It should be possible to pass a sender address.
"""
self.protocol.probe(JID('user@example.com'),
sender=JID('user@example.org'))
element = self.output[-1]
self.assertEquals("user@example.org", element.getAttribute('from'))
class RosterItemTest(unittest.TestCase):
"""
Tests for L{xmppim.RosterItem}.
"""
def test_toElement(self):
"""
A roster item has the correct namespace/name, lacks unset attributes.
"""
item = xmppim.RosterItem(JID('user@example.org'))
element = item.toElement()
self.assertEqual('item', element.name)
self.assertEqual(NS_ROSTER, element.uri)
self.assertFalse(element.hasAttribute('subscription'))
self.assertFalse(element.hasAttribute('ask'))
self.assertEqual(u"", element.getAttribute('name', u""))
self.assertFalse(element.hasAttribute('approved'))
self.assertEquals(0, len(list(element.elements())))
def test_toElementMinimal(self):
"""
A bare roster item only has a jid attribute.
"""
item = xmppim.RosterItem(JID('user@example.org'))
element = item.toElement()
self.assertEqual(u'user@example.org', element.getAttribute('jid'))
def test_toElementSubscriptionNone(self):
"""
A roster item with no subscription has no subscription attribute.
"""
item = xmppim.RosterItem(JID('user@example.org'),
subscriptionTo=False,
subscriptionFrom=False)
element = item.toElement()
self.assertIdentical(None, element.getAttribute('subscription'))
def test_toElementSubscriptionTo(self):
"""
A roster item with subscriptionTo set has subscription 'to'.
"""
item = xmppim.RosterItem(JID('user@example.org'),
subscriptionTo=True,
subscriptionFrom=False)
element = item.toElement()
self.assertEqual('to', element.getAttribute('subscription'))
def test_toElementSubscriptionFrom(self):
"""
A roster item with subscriptionFrom set has subscription 'to'.
"""
item = xmppim.RosterItem(JID('user@example.org'),
subscriptionTo=False,
subscriptionFrom=True)
element = item.toElement()
self.assertEqual('from', element.getAttribute('subscription'))
def test_toElementSubscriptionBoth(self):
"""
A roster item with mutual subscription has subscription 'both'.
"""
item = xmppim.RosterItem(JID('user@example.org'),
subscriptionTo=True,
subscriptionFrom=True)
element = item.toElement()
self.assertEqual('both', element.getAttribute('subscription'))
def test_toElementSubscriptionRemove(self):
"""
A roster item with remove set has subscription 'remove'.
"""
item = xmppim.RosterItem(JID('user@example.org'))
item.remove = True
element = item.toElement()
self.assertEqual('remove', element.getAttribute('subscription'))
def test_toElementAsk(self):
"""
A roster item with pendingOut set has subscription 'ask'.
"""
item = xmppim.RosterItem(JID('user@example.org'))
item.pendingOut = True
element = item.toElement()
self.assertEqual('subscribe', element.getAttribute('ask'))
def test_toElementName(self):
"""
A roster item's name is rendered to the 'name' attribute.
"""
item = xmppim.RosterItem(JID('user@example.org'),
name='Joe User')
element = item.toElement()
self.assertEqual(u'Joe User', element.getAttribute('name'))
def test_toElementGroups(self):
"""
A roster item's groups are rendered as 'group' child elements.
"""
groups = set(['Friends', 'Jabber'])
item = xmppim.RosterItem(JID('user@example.org'),
groups=groups)
element = item.toElement()
foundGroups = set()
for child in element.elements():
if child.uri == NS_ROSTER and child.name == 'group':
foundGroups.add(unicode(child))
self.assertEqual(groups, foundGroups)
def test_toElementApproved(self):
"""
A pre-approved subscription for a roster item has an 'approved' flag.
"""
item = xmppim.RosterItem(JID('user@example.org'))
item.approved = True
element = item.toElement()
self.assertEqual(u'true', element.getAttribute('approved'))
def test_fromElementMinimal(self):
"""
A minimal roster item has a reference to the JID of the contact.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertEqual(JID(u"test@example.org"), item.entity)
self.assertEqual(u"", item.name)
self.assertFalse(item.subscriptionTo)
self.assertFalse(item.subscriptionFrom)
self.assertFalse(item.pendingOut)
self.assertFalse(item.approved)
self.assertEqual(set(), item.groups)
def test_fromElementName(self):
"""
A roster item may have an optional name.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertEqual(u"Test User", item.name)
def test_fromElementGroups(self):
"""
A roster item may have one or more groups.
"""
xml = """
-
Friends
Twisted
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertIn(u"Twisted", item.groups)
self.assertIn(u"Friends", item.groups)
def test_fromElementSubscriptionNone(self):
"""
Subscription 'none' sets both attributes to False.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertFalse(item.remove)
self.assertFalse(item.subscriptionTo)
self.assertFalse(item.subscriptionFrom)
def test_fromElementSubscriptionTo(self):
"""
Subscription 'to' sets the corresponding attribute to True.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertFalse(item.remove)
self.assertTrue(item.subscriptionTo)
self.assertFalse(item.subscriptionFrom)
def test_fromElementSubscriptionFrom(self):
"""
Subscription 'from' sets the corresponding attribute to True.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertFalse(item.remove)
self.assertFalse(item.subscriptionTo)
self.assertTrue(item.subscriptionFrom)
def test_fromElementSubscriptionBoth(self):
"""
Subscription 'both' sets both attributes to True.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertFalse(item.remove)
self.assertTrue(item.subscriptionTo)
self.assertTrue(item.subscriptionFrom)
def test_fromElementSubscriptionRemove(self):
"""
Subscription 'remove' sets the remove attribute.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertTrue(item.remove)
def test_fromElementPendingOut(self):
"""
The ask attribute, if set to 'subscription', means pending out.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertTrue(item.pendingOut)
def test_fromElementApprovedTrue(self):
"""
The approved attribute (true) signals a pre-approved subscription.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertTrue(item.approved)
def test_fromElementApproved1(self):
"""
The approved attribute (1) signals a pre-approved subscription.
"""
xml = """
"""
item = xmppim.RosterItem.fromElement(parseXml(xml))
self.assertTrue(item.approved)
def test_jidDeprecationGet(self):
"""
Getting the jid attribute works as entity and warns deprecation.
"""
item = xmppim.RosterItem(JID('user@example.org'))
entity = self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterItem.jid was "
"deprecated in Wokkel 0.7.1; "
"please use RosterItem.entity instead.",
xmppim.__file__,
getattr, item, 'jid')
self.assertIdentical(entity, item.entity)
def test_jidDeprecationSet(self):
"""
Setting the jid attribute works as entity and warns deprecation.
"""
item = xmppim.RosterItem(JID('user@example.org'))
self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterItem.jid was deprecated "
"in Wokkel 0.7.1; "
"please use RosterItem.entity instead.",
xmppim.__file__,
setattr, item, 'jid',
JID('other@example.org'))
self.assertEqual(JID('other@example.org'), item.entity)
def test_askDeprecationGet(self):
"""
Getting the ask attribute works as entity and warns deprecation.
"""
item = xmppim.RosterItem(JID('user@example.org'))
item.pendingOut = True
ask = self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterItem.ask was "
"deprecated in Wokkel 0.7.1; "
"please use RosterItem.pendingOut instead.",
xmppim.__file__,
getattr, item, 'ask')
self.assertTrue(ask)
def test_askDeprecationSet(self):
"""
Setting the ask attribute works as entity and warns deprecation.
"""
item = xmppim.RosterItem(JID('user@example.org'))
self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterItem.ask was "
"deprecated in Wokkel 0.7.1; "
"please use RosterItem.pendingOut instead.",
xmppim.__file__,
setattr, item, 'ask',
True)
self.assertTrue(item.pendingOut)
class RosterRequestTest(unittest.TestCase):
"""
Tests for L{xmppim.RosterRequest}.
"""
def test_fromElement(self):
"""
A bare roster request is parsed and missing information is None.
"""
xml = """
"""
request = xmppim.RosterRequest.fromElement(parseXml(xml))
self.assertEqual('get', request.stanzaType)
self.assertEqual(JID('this@example.org/Home'), request.recipient)
self.assertEqual(JID('this@example.org'), request.sender)
self.assertEqual(None, request.item)
self.assertEqual(None, request.version)
def test_fromElementItem(self):
"""
If an item is present, parse it and put it in the request item.
"""
xml = """
"""
request = xmppim.RosterRequest.fromElement(parseXml(xml))
self.assertNotIdentical(None, request.item)
self.assertEqual(JID('user@example.org'), request.item.entity)
def test_fromElementVersion(self):
"""
If a ver attribute is present, put it in the request version.
"""
xml = """
"""
request = xmppim.RosterRequest.fromElement(parseXml(xml))
self.assertEqual('ver72', request.version)
def test_fromElementVersionEmpty(self):
"""
The ver attribute may be empty.
"""
xml = """
"""
request = xmppim.RosterRequest.fromElement(parseXml(xml))
self.assertEqual('', request.version)
def test_toElement(self):
"""
A roster request has a query element in the roster namespace.
"""
request = xmppim.RosterRequest()
element = request.toElement()
children = element.elements()
child = children.next()
self.assertEqual(NS_ROSTER, child.uri)
self.assertEqual('query', child.name)
def test_toElementItem(self):
"""
If an item is set, it is rendered as a child of the query.
"""
request = xmppim.RosterRequest()
request.item = xmppim.RosterItem(JID('user@example.org'))
element = request.toElement()
children = element.query.elements()
child = children.next()
self.assertEqual(NS_ROSTER, child.uri)
self.assertEqual('item', child.name)
class FakeClient(object):
"""
Fake client stream manager for roster tests.
"""
def __init__(self, xmlstream, jid):
self.xmlstream = xmlstream
self.jid = jid
def request(self, request):
element = request.toElement()
self.xmlstream.send(element)
return defer.Deferred()
def addHandler(self, handler):
handler.makeConnection(self.xmlstream)
handler.connectionInitialized()
def test_toElementVersion(self):
"""
If the roster version is set, a 'ver' attribute is added.
"""
request = xmppim.RosterRequest()
request.version = 'ver72'
element = request.toElement()
self.assertEqual('ver72', element.query.getAttribute('ver'))
def test_toElementVersionEmpty(self):
"""
If the roster version is the empty string, it should add 'ver', too.
"""
request = xmppim.RosterRequest()
request.version = ''
element = request.toElement()
self.assertEqual('', element.query.getAttribute('ver'))
class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
"""
Tests for L{xmppim.RosterClientProtocol}.
"""
def setUp(self):
self.stub = XmlStreamStub()
self.client = FakeClient(self.stub.xmlstream, JID('this@example.org'))
self.service = xmppim.RosterClientProtocol()
self.service.setHandlerParent(self.client)
def test_setItem(self):
"""
Setting a roster item renders the item and sends it out.
"""
item = xmppim.RosterItem(JID('test@example.org'),
name='Joe User',
groups=set(['Friends', 'Jabber']))
d = self.service.setItem(item)
# Inspect outgoing iq request
iq = self.stub.output[-1]
self.assertEqual('set', iq.getAttribute('type'))
self.assertNotIdentical(None, iq.query)
self.assertEqual(NS_ROSTER, iq.query.uri)
children = list(domish.generateElementsQNamed(iq.query.children,
'item', NS_ROSTER))
self.assertEqual(1, len(children))
child = children[0]
self.assertEqual('test@example.org', child['jid'])
self.assertIdentical(None, child.getAttribute('subscription'))
# Fake successful response
response = toResponse(iq, 'result')
d.callback(response)
return d
def test_setItemIgnoreAttributes(self):
"""
Certain attributes should be rendered for roster set.
"""
item = xmppim.RosterItem(JID('test@example.org'),
subscriptionTo=True,
subscriptionFrom=False,
name='Joe User',
groups=set(['Friends', 'Jabber']))
item.pendingOut = True
item.approved = True
d = self.service.setItem(item)
# Inspect outgoing iq request
iq = self.stub.output[-1]
self.assertEqual('set', iq.getAttribute('type'))
self.assertNotIdentical(None, iq.query)
self.assertEqual(NS_ROSTER, iq.query.uri)
children = list(domish.generateElementsQNamed(iq.query.children,
'item', NS_ROSTER))
self.assertEqual(1, len(children))
child = children[0]
self.assertIdentical(None, child.getAttribute('ask'))
self.assertIdentical(None, child.getAttribute('approved'))
self.assertIdentical(None, child.getAttribute('subscription'))
# Fake successful response
response = toResponse(iq, 'result')
d.callback(response)
return d
def test_removeItem(self):
"""
Removing a roster item is setting an item with subscription C{remove}.
"""
d = self.service.removeItem(JID('test@example.org'))
# Inspect outgoing iq request
iq = self.stub.output[-1]
self.assertEqual('set', iq.getAttribute('type'))
self.assertNotIdentical(None, iq.query)
self.assertEqual(NS_ROSTER, iq.query.uri)
children = list(domish.generateElementsQNamed(iq.query.children,
'item', NS_ROSTER))
self.assertEqual(1, len(children))
child = children[0]
self.assertEqual('test@example.org', child['jid'])
self.assertEqual('remove', child.getAttribute('subscription'))
# Fake successful response
response = toResponse(iq, 'result')
d.callback(response)
return d
def test_getRoster(self):
"""
A request for the roster is sent out and the response is parsed.
"""
def cb(roster):
self.assertIn(JID('user@example.org'), roster)
self.assertIdentical(None, getattr(roster, 'version'))
d = self.service.getRoster()
d.addCallback(cb)
# Inspect outgoing iq request
iq = self.stub.output[-1]
self.assertEqual('get', iq.getAttribute('type'))
self.assertNotIdentical(None, iq.query)
self.assertEqual(NS_ROSTER, iq.query.uri)
self.assertFalse(iq.query.hasAttribute('ver'))
# Fake successful response
response = toResponse(iq, 'result')
query = response.addElement((NS_ROSTER, 'query'))
item = query.addElement('item')
item['jid'] = 'user@example.org'
d.callback(response)
return d
def test_getRosterVer(self):
"""
A request for the roster with version passes the version on.
"""
def cb(roster):
self.assertEqual('ver96', getattr(roster, 'version'))
d = self.service.getRoster(version='ver72')
d.addCallback(cb)
# Inspect outgoing iq request
iq = self.stub.output[-1]
self.assertEqual('ver72', iq.query.getAttribute('ver'))
# Fake successful response
response = toResponse(iq, 'result')
query = response.addElement((NS_ROSTER, 'query'))
query['ver'] = 'ver96'
item = query.addElement('item')
item['jid'] = 'user@example.org'
d.callback(response)
return d
def test_getRosterVerEmptyResult(self):
"""
An empty response is returned as None.
"""
def cb(response):
self.assertIdentical(None, response)
d = self.service.getRoster(version='ver72')
d.addCallback(cb)
# Inspect outgoing iq request
iq = self.stub.output[-1]
# Fake successful response
response = toResponse(iq, 'result')
d.callback(response)
return d
def test_onRosterSet(self):
"""
A roster push causes onRosterSet to be called with the parsed item.
"""
xml = """
"""
items = []
def onRosterSet(item):
items.append(item)
def cb(result):
self.assertEqual(1, len(items))
self.assertEqual(JID('user@example.org'), items[0].entity)
self.service.onRosterSet = onRosterSet
d = self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterClientProtocol.onRosterSet "
"was deprecated in Wokkel 0.7.1; "
"please use RosterClientProtocol.setReceived "
"instead.",
xmppim.__file__,
self.handleRequest, xml)
d.addCallback(cb)
return d
def test_onRosterRemove(self):
"""
A roster push causes onRosterSet to be called with the parsed item.
"""
xml = """
"""
entities = []
def onRosterRemove(entity):
entities.append(entity)
def cb(result):
self.assertEqual([JID('user@example.org')], entities)
self.service.onRosterRemove = onRosterRemove
d = self.assertWarns(DeprecationWarning,
"wokkel.xmppim.RosterClientProtocol.onRosterRemove "
"was deprecated in Wokkel 0.7.1; "
"please use RosterClientProtocol.removeReceived "
"instead.",
xmppim.__file__,
self.handleRequest, xml)
d.addCallback(cb)
return d
def test_setReceived(self):
"""
A roster set push causes setReceived.
"""
xml = """
"""
requests = []
def setReceived(request):
requests.append(request)
def cb(result):
self.assertEqual(1, len(requests), "setReceived was not called")
self.assertEqual(JID('user@example.org'), requests[0].item.entity)
self.service.setReceived = setReceived
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_setReceivedOtherSource(self):
"""
Roster pushes can be sent from other entities, too, ignore them.
"""
xml = """
"""
def cb(result):
self.assertEquals('service-unavailable', result.condition)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_setReceivedOtherSourceAllowed(self):
"""
Roster pushes can be sent from other entities, allow them.
"""
xml = """
"""
self.service.allowAnySender = True
requests = []
def setReceived(request):
requests.append(request)
def cb(result):
self.assertEqual(1, len(requests), "setReceived was not called")
self.service.setReceived = setReceived
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_setReceivedOtherSourceIgnored(self):
"""
Roster pushes can be sent from other entities, allow them.
"""
xml = """
"""
self.service.allowAnySender = True
def setReceived(request):
if request.sender == JID('bad@example.org'):
raise xmppim.RosterPushIgnored()
def cb(result):
self.assertEquals('service-unavailable', result.condition)
self.service.setReceived = setReceived
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_removeReceived(self):
"""
A roster remove push causes removeReceived.
"""
xml = """
"""
requests = []
def removeReceived(request):
requests.append(request)
def cb(result):
self.assertEqual(1, len(requests), "removeReceived was not called")
self.assertEqual(JID('user@example.org'), requests[0].item.entity)
self.service.removeReceived = removeReceived
d = self.handleRequest(xml)
d.addCallback(cb)
return d
wokkel-0.7.1/wokkel/test/test_iwokkel.py 0000664 0001750 0001750 00000003132 11707274572 021151 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.iwokkel}
"""
from twisted.trial import unittest
class DeprecationTest(unittest.TestCase):
"""
Deprecation test for L{wokkel.subprotocols}.
"""
def lookForDeprecationWarning(self, testmethod, attributeName, newName):
"""
Importing C{testmethod} emits a deprecation warning.
"""
warningsShown = self.flushWarnings([testmethod])
self.assertEqual(len(warningsShown), 1)
self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
self.assertEqual(
warningsShown[0]['message'],
"wokkel.iwokkel." + attributeName + " "
"was deprecated in Wokkel 0.7.0: Use " + newName + " instead.")
def test_iXMPPHandler(self):
"""
L{wokkel.iwokkel.IXMPPHandler} is deprecated.
"""
from wokkel.iwokkel import IXMPPHandler
IXMPPHandler
self.lookForDeprecationWarning(
self.test_iXMPPHandler,
"IXMPPHandler",
"twisted.words.protocols.jabber.ijabber."
"IXMPPHandler")
def test_iXMPPHandlerCollection(self):
"""
L{wokkel.iwokkel.IXMPPHandlerCollection} is deprecated.
"""
from wokkel.iwokkel import IXMPPHandlerCollection
IXMPPHandlerCollection
self.lookForDeprecationWarning(
self.test_iXMPPHandlerCollection,
"IXMPPHandlerCollection",
"twisted.words.protocols.jabber.ijabber."
"IXMPPHandlerCollection")
wokkel-0.7.1/wokkel/test/test_delay.py 0000664 0001750 0001750 00000014637 11707215356 020611 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.delay}.
"""
from datetime import datetime
import dateutil.tz
from twisted.trial import unittest
from twisted.words.protocols.jabber.jid import JID
from wokkel.delay import Delay, DelayMixin
from wokkel.generic import Stanza, parseXml
class DelayTest(unittest.TestCase):
"""
Tests for L{delay.Delay}.
"""
def test_toElement(self):
"""
The DOM structure has the serialized timestamp and sender address.
"""
delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25,
tzinfo=dateutil.tz.tzutc()),
sender=JID(u'user@example.org'))
element = delay.toElement()
self.assertEqual(u'urn:xmpp:delay', element.uri)
self.assertEqual(u'delay', element.name)
self.assertEqual(u'2002-09-10T23:08:25Z', element.getAttribute('stamp'))
self.assertEqual(u'user@example.org', element.getAttribute('from'))
def test_toElementStampMissing(self):
"""
To render to XML, at least a timestamp must be provided.
"""
delay = Delay(stamp=None)
self.assertRaises(ValueError, delay.toElement)
def test_toElementStampOffsetNaive(self):
"""
The provided timestamp must be offset aware.
"""
delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25))
self.assertRaises(ValueError, delay.toElement)
def test_toElementLegacy(self):
"""
The legacy format uses C{CCYYMMDDThh:mm:ss} in the old namespace.
"""
delay = Delay(stamp=datetime(2002, 9, 10, 23, 8, 25,
tzinfo=dateutil.tz.tzutc()),
sender=JID(u'user@example.org'))
element = delay.toElement(legacy=True)
self.assertEqual(u'jabber:x:delay', element.uri)
self.assertEqual(u'x', element.name)
self.assertEqual(u'20020910T23:08:25', element.getAttribute('stamp'))
self.assertEqual(u'user@example.org', element.getAttribute('from'))
def test_fromElement(self):
"""
The timestamp is parsed with the proper timezone (UTC).
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertEqual(datetime(2002, 9, 10, 23, 8, 25,
tzinfo=dateutil.tz.tzutc()),
delay.stamp)
self.assertIdentical(None, delay.sender)
def test_fromElementLegacy(self):
"""
For legacy XEP-0091 support, the timestamp is assumed to be in UTC.
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertEqual(datetime(2002, 9, 10, 23, 8, 25,
tzinfo=dateutil.tz.tzutc()),
delay.stamp)
self.assertIdentical(None, delay.sender)
def test_fromElementSender(self):
"""
The optional original sender address is parsed as a JID.
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertEqual(JID(u'user@example.org'), delay.sender)
def test_fromElementSenderBad(self):
"""
An invalid original sender address results in C{None}.
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertIdentical(None, delay.sender)
def test_fromElementMissingStamp(self):
"""
A missing timestamp results in C{None} for the stamp attribute.
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertIdentical(None, delay.stamp)
def test_fromElementBadStamp(self):
"""
A malformed timestamp results in C{None} for the stamp attribute.
"""
xml = parseXml(u"""
""")
delay = Delay.fromElement(xml)
self.assertIdentical(None, delay.stamp)
class DelayStanza(Stanza, DelayMixin):
"""
Test stanza class that mixes in delayed delivery information parsing.
"""
class DelayMixinTest(unittest.TestCase):
def test_fromParentElement(self):
"""
A child element with delay information is found and parsed.
"""
xml = parseXml(u"""
""")
stanza = DelayStanza.fromElement(xml)
self.assertNotIdentical(None, stanza.delay)
def test_fromParentElementLegacy(self):
"""
A child element with legacy delay information is found and parsed.
"""
xml = parseXml(u"""
""")
stanza = DelayStanza.fromElement(xml)
self.assertNotIdentical(None, stanza.delay)
def test_fromParentElementBothLegacyLast(self):
"""
The XEP-0203 format is used over later legacy XEP-0091 format.
"""
xml = parseXml(u"""
""")
stanza = DelayStanza.fromElement(xml)
self.assertNotIdentical(None, stanza.delay)
self.assertEqual(2002, stanza.delay.stamp.year)
def test_fromParentElementBothLegacyFirst(self):
"""
The XEP-0203 format is used over earlier legacy XEP-0091 format.
"""
xml = parseXml(u"""
""")
stanza = DelayStanza.fromElement(xml)
self.assertNotIdentical(None, stanza.delay)
self.assertEqual(2002, stanza.delay.stamp.year)
wokkel-0.7.1/wokkel/test/test_muc.py 0000664 0001750 0001750 00000172044 11707215356 020274 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.muc}
"""
from datetime import datetime
from dateutil.tz import tzutc
from zope.interface import verify
from twisted.trial import unittest
from twisted.internet import defer, task
from twisted.words.xish import domish, xpath
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.protocols.jabber.xmlstream import TimeoutError, toResponse
from wokkel import data_form, delay, iwokkel, muc
from wokkel.generic import parseXml
from wokkel.test.helpers import TestableStreamManager
NS_MUC_ADMIN = 'http://jabber.org/protocol/muc#admin'
def calledAsync(fn):
"""
Function wrapper that fires a deferred upon calling the given function.
"""
d = defer.Deferred()
def func(*args, **kwargs):
try:
result = fn(*args, **kwargs)
except:
d.errback()
else:
d.callback(result)
return d, func
class StatusCodeTest(unittest.TestCase):
"""
Tests for L{muc.STATUS_CODE}.
"""
def test_lookupByValue(self):
"""
The registered MUC status codes map to STATUS_CODE value constants.
Note: the identifiers used in the dictionary of status codes are
borrowed from U{XEP-0306}
that defines Extensible Status Conditions for Multi-User Chat. If this
specification is implemented itself, the dictionary could move there.
"""
codes = {
100: 'realjid-public',
101: 'affiliation-changed',
102: 'unavailable-shown',
103: 'unavailable-not-shown',
104: 'configuration-changed',
110: 'self-presence',
170: 'logging-enabled',
171: 'logging-disabled',
172: 'non-anonymous',
173: 'semi-anonymous',
174: 'fully-anonymous',
201: 'room-created',
210: 'nick-assigned',
301: 'banned',
303: 'new-nick',
307: 'kicked',
321: 'removed-affiliation',
322: 'removed-membership',
332: 'removed-shutdown',
}
for code, condition in codes.iteritems():
constantName = condition.replace('-', '_').upper()
self.assertEqual(getattr(muc.STATUS_CODE, constantName),
muc.STATUS_CODE.lookupByValue(code))
class StatusesTest(unittest.TestCase):
"""
Tests for L{muc.Statuses}.
"""
def setUp(self):
self.mucStatuses = muc.Statuses()
self.mucStatuses.add(muc.STATUS_CODE.SELF_PRESENCE)
self.mucStatuses.add(muc.STATUS_CODE.ROOM_CREATED)
def test_interface(self):
"""
Instances of L{Statuses} provide L{iwokkel.IMUCStatuses}.
"""
verify.verifyObject(iwokkel.IMUCStatuses, self.mucStatuses)
def test_contains(self):
"""
The status contained are 'in' the container.
"""
self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, self.mucStatuses)
self.assertIn(muc.STATUS_CODE.ROOM_CREATED, self.mucStatuses)
self.assertNotIn(muc.STATUS_CODE.NON_ANONYMOUS, self.mucStatuses)
def test_iter(self):
"""
All statuses can be iterated over.
"""
statuses = set()
for status in self.mucStatuses:
statuses.add(status)
self.assertEqual(set([muc.STATUS_CODE.SELF_PRESENCE,
muc.STATUS_CODE.ROOM_CREATED]), statuses)
def test_len(self):
"""
The number of items in this container is returned by C{__len__}.
"""
self.assertEqual(2, len(self.mucStatuses))
class GroupChatTest(unittest.TestCase):
"""
Tests for L{muc.GroupChat}.
"""
def test_toElementDelay(self):
"""
If the delay attribute is set, toElement has it rendered.
"""
message = muc.GroupChat()
message.delay = delay.Delay(stamp=datetime(2002, 10, 13, 23, 58, 37,
tzinfo=tzutc()))
element = message.toElement()
query = "/message/delay[@xmlns='%s']" % (delay.NS_DELAY,)
nodes = xpath.queryForNodes(query, element)
self.assertNotIdentical(None, nodes, "Missing delay element")
def test_toElementDelayLegacy(self):
"""
If legacy delay is requested, the legacy format is rendered.
"""
message = muc.GroupChat()
message.delay = delay.Delay(stamp=datetime(2002, 10, 13, 23, 58, 37,
tzinfo=tzutc()))
element = message.toElement(legacyDelay=True)
query = "/message/x[@xmlns='%s']" % (delay.NS_JABBER_DELAY,)
nodes = xpath.queryForNodes(query, element)
self.assertNotIdentical(None, nodes, "Missing legacy delay element")
class HistoryOptionsTest(unittest.TestCase):
"""
Tests for L{muc.HistoryOptionsTest}.
"""
def test_toElement(self):
"""
toElement renders the history element in the right namespace.
"""
history = muc.HistoryOptions()
element = history.toElement()
self.assertEqual(muc.NS_MUC, element.uri)
self.assertEqual('history', element.name)
def test_toElementMaxStanzas(self):
"""
If C{maxStanzas} is set, the element has the attribute C{'maxstanzas'}.
"""
history = muc.HistoryOptions(maxStanzas=10)
element = history.toElement()
self.assertEqual(u'10', element.getAttribute('maxstanzas'))
def test_toElementSince(self):
"""
If C{since} is set, the attribute C{'since'} has a rendered timestamp.
"""
history = muc.HistoryOptions(since=datetime(2002, 10, 13, 23, 58, 37,
tzinfo=tzutc()))
element = history.toElement()
self.assertEqual(u'2002-10-13T23:58:37Z',
element.getAttribute('since'))
class UserPresenceTest(unittest.TestCase):
"""
Tests for L{muc.UserPresence}.
"""
def test_fromElementNoUserElement(self):
"""
Without user element, all associated attributes are None.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertIdentical(None, presence.affiliation)
self.assertIdentical(None, presence.role)
self.assertIdentical(None, presence.entity)
self.assertIdentical(None, presence.nick)
self.assertEqual(0, len(presence.mucStatuses))
def test_fromElementUnknownChild(self):
"""
Unknown child elements are ignored.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertEqual(0, len(presence.mucStatuses))
def test_fromElementStatusOne(self):
"""
Status codes are extracted.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, presence.mucStatuses)
def test_fromElementStatusMultiple(self):
"""
Multiple status codes are all extracted.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertIn(muc.STATUS_CODE.SELF_PRESENCE, presence.mucStatuses)
self.assertIn(muc.STATUS_CODE.REALJID_PUBLIC, presence.mucStatuses)
def test_fromElementStatusEmpty(self):
"""
Empty status elements are ignored.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertEqual(0, len(presence.mucStatuses))
def test_fromElementStatusBad(self):
"""
Bad status codes are ignored.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertEqual(0, len(presence.mucStatuses))
def test_fromElementStatusUnknown(self):
"""
Unknown status codes are not recorded in C{mucStatuses}.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertEqual(0, len(presence.mucStatuses))
def test_fromElementItem(self):
"""
Item attributes are parsed properly.
"""
xml = """
"""
element = parseXml(xml)
presence = muc.UserPresence.fromElement(element)
self.assertEqual(u'member', presence.affiliation)
self.assertEqual(u'participant', presence.role)
self.assertEqual(JID('hag66@shakespeare.lit/pda'), presence.entity)
self.assertEqual(u'thirdwitch', presence.nick)
class MUCClientProtocolTest(unittest.TestCase):
"""
Tests for L{muc.MUCClientProtocol}.
"""
def setUp(self):
self.clock = task.Clock()
self.sessionManager = TestableStreamManager(reactor=self.clock)
self.stub = self.sessionManager.stub
self.protocol = muc.MUCClientProtocol(reactor=self.clock)
self.protocol.setHandlerParent(self.sessionManager)
self.roomIdentifier = 'test'
self.service = 'conference.example.org'
self.nick = 'Nick'
self.occupantJID = JID(tuple=(self.roomIdentifier,
self.service,
self.nick))
self.roomJID = self.occupantJID.userhostJID()
self.userJID = JID('test@example.org/Testing')
def test_initNoReactor(self):
"""
If no reactor is passed, the default reactor is used.
"""
protocol = muc.MUCClientProtocol()
from twisted.internet import reactor
self.assertEqual(reactor, protocol._reactor)
def test_groupChatReceived(self):
"""
Messages of type groupchat are parsed and passed to L{groupChatReceived}.
"""
xml = u"""
test
""" % (self.occupantJID)
def groupChatReceived(message):
self.assertEquals('test', message.body, "Wrong group chat message")
self.assertEquals(self.roomIdentifier, message.sender.user,
'Wrong room identifier')
d, self.protocol.groupChatReceived = calledAsync(groupChatReceived)
self.stub.send(parseXml(xml))
return d
def test_groupChatReceivedNotOverridden(self):
"""
If L{groupChatReceived} has not been overridden, no errors should occur.
"""
xml = u"""
test
""" % (self.occupantJID)
self.stub.send(parseXml(xml))
def test_join(self):
"""
Joining a room waits for confirmation, deferred fires user presence.
"""
def cb(presence):
self.assertEquals(self.occupantJID, presence.sender)
# Join the room
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(cb)
element = self.stub.output[-1]
self.assertEquals('presence', element.name, "Need to be presence")
self.assertNotIdentical(None, element.x, 'No muc x element')
# send back user presence, they joined
xml = """
""" % (self.roomIdentifier, self.service, self.nick)
self.stub.send(parseXml(xml))
return d
def test_joinHistory(self):
"""
Passing a history parameter sends a 'maxStanzas' history limit.
"""
historyOptions = muc.HistoryOptions(maxStanzas=10)
d = self.protocol.join(self.roomJID, self.nick,
historyOptions)
element = self.stub.output[-1]
query = "/*/x[@xmlns='%s']/history[@xmlns='%s']" % (muc.NS_MUC,
muc.NS_MUC)
result = xpath.queryForNodes(query, element)
history = result[0]
self.assertEquals('10', history.getAttribute('maxstanzas'))
# send back user presence, they joined
xml = """
""" % (self.roomIdentifier, self.service, self.nick)
self.stub.send(parseXml(xml))
return d
def test_joinForbidden(self):
"""
A forbidden error in response to a join errbacks with L{StanzaError}.
"""
def cb(error):
self.assertEquals('forbidden', error.condition,
'Wrong muc condition')
d = self.protocol.join(self.roomJID, self.nick)
self.assertFailure(d, StanzaError)
d.addCallback(cb)
# send back error, forbidden
xml = u"""
""" % (self.occupantJID)
self.stub.send(parseXml(xml))
return d
def test_joinForbiddenFromRoomJID(self):
"""
An error response to a join sent from the room JID should errback.
Some service implementations send error stanzas from the room JID
instead of the JID the join presence was sent to.
"""
d = self.protocol.join(self.roomJID, self.nick)
self.assertFailure(d, StanzaError)
# send back error, forbidden
xml = u"""
""" % (self.roomJID)
self.stub.send(parseXml(xml))
return d
def test_joinBadJID(self):
"""
Client joining a room and getting a jid-malformed error.
"""
def cb(error):
self.assertEquals('jid-malformed', error.condition,
'Wrong muc condition')
d = self.protocol.join(self.roomJID, self.nick)
self.assertFailure(d, StanzaError)
d.addCallback(cb)
# send back error, bad JID
xml = u"""
""" % (self.occupantJID)
self.stub.send(parseXml(xml))
return d
def test_joinTimeout(self):
"""
After not receiving a response to a join, errback with L{TimeoutError}.
"""
d = self.protocol.join(self.roomJID, self.nick)
self.assertFailure(d, TimeoutError)
self.clock.advance(muc.DEFER_TIMEOUT)
return d
def test_joinPassword(self):
"""
Sending a password via presence to a password protected room.
"""
self.protocol.join(self.roomJID, self.nick, password='secret')
element = self.stub.output[-1]
self.assertTrue(xpath.matches(
u"/presence[@to='%s']/x/password"
"[text()='secret']" % (self.occupantJID,),
element),
'Wrong presence stanza')
def test_nick(self):
"""
Send a nick change to the server.
"""
newNick = 'newNick'
def cb(presence):
self.assertEquals(JID(tuple=(self.roomIdentifier,
self.service,
newNick)),
presence.sender)
d = self.protocol.nick(self.roomJID, newNick)
d.addCallback(cb)
element = self.stub.output[-1]
self.assertEquals('presence', element.name, "Need to be presence")
self.assertNotIdentical(None, element.x, 'No muc x element')
# send back user presence, nick changed
xml = u"""
""" % (self.roomJID, newNick)
self.stub.send(parseXml(xml))
return d
def test_nickConflict(self):
"""
If the server finds the new nick in conflict, the errback is called.
"""
newNick = 'newNick'
d = self.protocol.nick(self.roomJID, newNick)
self.assertFailure(d, StanzaError)
element = self.stub.output[-1]
self.assertEquals('presence', element.name, "Need to be presence")
self.assertNotIdentical(None, element.x, 'No muc x element')
# send back error presence, nick conflicted
xml = u"""
""" % (self.roomJID, newNick)
self.stub.send(parseXml(xml))
return d
def test_status(self):
"""
Change status
"""
def joined(_):
d = self.protocol.status(self.roomJID, 'xa', 'testing MUC')
d.addCallback(statusChanged)
return d
def statusChanged(presence):
self.assertEqual(self.occupantJID, presence.sender)
# Join the room
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(joined)
# Receive presence back from the room: joined.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
# The presence for the status change should have been sent now.
element = self.stub.output[-1]
self.assertEquals('presence', element.name, "Need to be presence")
self.assertTrue(getattr(element, 'x', None), 'No muc x element')
# send back user presence, status changed
xml = u"""
xa
testing MUC
""" % self.occupantJID
self.stub.send(parseXml(xml))
return d
def test_leave(self):
"""
Client leaves a room
"""
def joined(_):
return self.protocol.leave(self.roomJID)
# Join the room
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(joined)
# Receive presence back from the room: joined.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
# The presence for leaving the room should have been sent now.
element = self.stub.output[-1]
self.assertEquals('unavailable', element['type'],
'Unavailable is not being sent')
# Receive presence back from the room: left.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
return d
def test_groupChat(self):
"""
Send private messages to muc entities.
"""
self.protocol.groupChat(self.roomJID, u'This is a test')
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
self.assertEquals('groupchat', message.getAttribute('type'))
self.assertEquals(u'This is a test', unicode(message.body))
def test_chat(self):
"""
Send private messages to muc entities.
"""
otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick')
self.protocol.chat(otherOccupantJID, u'This is a test')
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertEquals(otherOccupantJID.full(), message.getAttribute('to'))
self.assertEquals('chat', message.getAttribute('type'))
self.assertEquals(u'This is a test', unicode(message.body))
def test_subject(self):
"""
Change subject of the room.
"""
self.protocol.subject(self.roomJID, u'This is a test')
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
self.assertEquals('groupchat', message.getAttribute('type'))
self.assertEquals(u'This is a test', unicode(message.subject))
def test_invite(self):
"""
Invite a user to a room
"""
invitee = JID('other@example.org')
self.protocol.invite(self.roomJID, invitee, u'This is a test')
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
self.assertEquals(muc.NS_MUC_USER, message.x.uri)
self.assertEquals(muc.NS_MUC_USER, message.x.invite.uri)
self.assertEquals(invitee.full(), message.x.invite.getAttribute('to'))
self.assertEquals(muc.NS_MUC_USER, message.x.invite.reason.uri)
self.assertEquals(u'This is a test', unicode(message.x.invite.reason))
def test_getRegisterForm(self):
"""
The response of a register form request should extract the form.
"""
def cb(form):
self.assertEquals('form', form.formType)
d = self.protocol.getRegisterForm(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % (muc.NS_REGISTER)
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Missing query element')
self.assertRaises(StopIteration, nodes[0].elements().next)
xml = u"""
http://jabber.org/protocol/muc#register
""" % (self.roomJID, iq['id'], self.userJID)
self.stub.send(parseXml(xml))
return d
def test_register(self):
"""
Client registering with a room.
http://xmpp.org/extensions/xep-0045.html#register
"""
def cb(iq):
# check for a result
self.assertEquals('result', iq['type'], 'We did not get a result')
d = self.protocol.register(self.roomJID,
{'muc#register_roomnick': 'thirdwitch'})
d.addCallback(cb)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % muc.NS_REGISTER
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Invalid registration request')
form = data_form.findForm(nodes[0], muc.NS_MUC_REGISTER)
self.assertNotIdentical(None, form, 'Missing registration form')
self.assertEquals('submit', form.formType)
self.assertIn('muc#register_roomnick', form.fields)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_registerCancel(self):
"""
Cancelling a registration request sends a cancel form.
"""
d = self.protocol.register(self.roomJID, None)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % muc.NS_REGISTER
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Invalid registration request')
form = data_form.findForm(nodes[0], muc.NS_MUC_REGISTER)
self.assertNotIdentical(None, form, 'Missing registration form')
self.assertEquals('cancel', form.formType)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_voice(self):
"""
Client requesting voice for a room.
"""
self.protocol.voice(self.occupantJID)
m = self.stub.output[-1]
query = ("/message/x[@type='submit']/field/value"
"[text()='%s']") % muc.NS_MUC_REQUEST
self.assertTrue(xpath.matches(query, m), 'Invalid voice message stanza')
def test_history(self):
"""
Converting a one to one chat to a multi-user chat.
"""
archive = []
thread = "e0ffe42b28561960c6b12b944a092794b9683a38"
# create messages
element = domish.Element((None, 'message'))
element['to'] = 'testing@example.com'
element['type'] = 'chat'
element.addElement('body', None, 'test')
element.addElement('thread', None, thread)
archive.append({'stanza': element,
'timestamp': datetime(2002, 10, 13, 23, 58, 37,
tzinfo=tzutc())})
element = domish.Element((None, 'message'))
element['to'] = 'testing2@example.com'
element['type'] = 'chat'
element.addElement('body', None, 'yo')
element.addElement('thread', None, thread)
archive.append({'stanza': element,
'timestamp': datetime(2002, 10, 13, 23, 58, 43,
tzinfo=tzutc())})
self.protocol.history(self.occupantJID, archive)
while len(self.stub.output)>0:
element = self.stub.output.pop()
# check for delay element
self.assertEquals('message', element.name, 'Wrong stanza')
self.assertTrue(xpath.matches("/message/delay", element),
'Invalid history stanza')
def test_getConfiguration(self):
"""
The response of a configure form request should extract the form.
"""
def cb(form):
self.assertEquals('form', form.formType)
d = self.protocol.getConfiguration(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER)
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Missing query element')
self.assertRaises(StopIteration, nodes[0].elements().next)
xml = u"""
http://jabber.org/protocol/muc#roomconfig
""" % (self.roomJID, iq['id'], self.userJID)
self.stub.send(parseXml(xml))
return d
def test_getConfigurationNoOptions(self):
"""
The response of a configure form request should extract the form.
"""
def cb(form):
self.assertIdentical(None, form)
d = self.protocol.getConfiguration(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
xml = u"""
""" % (self.roomJID, iq['id'], self.userJID)
self.stub.send(parseXml(xml))
return d
def test_configure(self):
"""
Default configure and changing the room name.
"""
def cb(iq):
self.assertEquals('result', iq['type'], 'Not a result')
values = {'muc#roomconfig_roomname': self.roomIdentifier}
d = self.protocol.configure(self.roomJID, values)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals(self.roomJID.full(), iq.getAttribute('to'))
query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER)
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Bad configure request')
form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG)
self.assertNotIdentical(None, form, 'Missing configuration form')
self.assertEquals('submit', form.formType)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_configureEmpty(self):
"""
Accept default configuration by sending an empty form.
"""
values = {}
d = self.protocol.configure(self.roomJID, values)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER)
nodes = xpath.queryForNodes(query, iq)
form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG)
self.assertNotIdentical(None, form, 'Missing configuration form')
self.assertEquals('submit', form.formType)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_configureCancel(self):
"""
Cancelling room configuration should send a cancel form.
"""
d = self.protocol.configure(self.roomJID, None)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']" % (muc.NS_MUC_OWNER)
nodes = xpath.queryForNodes(query, iq)
form = data_form.findForm(nodes[0], muc.NS_MUC_CONFIG)
self.assertNotIdentical(None, form, 'Missing configuration form')
self.assertEquals('cancel', form.formType)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_getMemberList(self):
"""
Retrieving the member list returns a list of L{muc.AdminItem}s
The request asks for the affiliation C{'member'}.
"""
def cb(items):
self.assertEquals(1, len(items))
item = items[0]
self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity)
self.assertEquals(u'thirdwitch', item.nick)
self.assertEquals(u'member', item.affiliation)
d = self.protocol.getMemberList(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('get', iq.getAttribute('type'))
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, items)
self.assertEquals(1, len(items))
self.assertEquals('member', items[0].getAttribute('affiliation'))
response = toResponse(iq, 'result')
query = response.addElement((NS_MUC_ADMIN, 'query'))
item = query.addElement('item')
item['affiliation'] ='member'
item['jid'] = 'hag66@shakespeare.lit'
item['nick'] = 'thirdwitch'
item['role'] = 'participant'
self.stub.send(response)
return d
def test_getAdminList(self):
"""
Retrieving the admin list returns a list of L{muc.AdminItem}s
The request asks for the affiliation C{'admin'}.
"""
d = self.protocol.getAdminList(self.roomJID)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertEquals('admin', items[0].getAttribute('affiliation'))
response = toResponse(iq, 'result')
query = response.addElement((NS_MUC_ADMIN, 'query'))
self.stub.send(response)
return d
def test_getBanList(self):
"""
Retrieving the ban list returns a list of L{muc.AdminItem}s
The request asks for the affiliation C{'outcast'}.
"""
def cb(items):
self.assertEquals(1, len(items))
item = items[0]
self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity)
self.assertEquals(u'outcast', item.affiliation)
self.assertEquals(u'Trouble making', item.reason)
d = self.protocol.getBanList(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertEquals('outcast', items[0].getAttribute('affiliation'))
response = toResponse(iq, 'result')
query = response.addElement((NS_MUC_ADMIN, 'query'))
item = query.addElement('item')
item['affiliation'] ='outcast'
item['jid'] = 'hag66@shakespeare.lit'
item.addElement('reason', content='Trouble making')
self.stub.send(response)
return d
def test_getOwnerList(self):
"""
Retrieving the owner list returns a list of L{muc.AdminItem}s
The request asks for the affiliation C{'owner'}.
"""
d = self.protocol.getOwnerList(self.roomJID)
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertEquals('owner', items[0].getAttribute('affiliation'))
response = toResponse(iq, 'result')
query = response.addElement((NS_MUC_ADMIN, 'query'))
self.stub.send(response)
return d
def test_getModeratorList(self):
"""
Retrieving the moderator returns a list of L{muc.AdminItem}s.
The request asks for the role C{'moderator'}.
"""
def cb(items):
self.assertEquals(1, len(items))
item = items[0]
self.assertEquals(JID(u'hag66@shakespeare.lit'), item.entity)
self.assertEquals(u'thirdwitch', item.nick)
self.assertEquals(u'moderator', item.role)
d = self.protocol.getModeratorList(self.roomJID)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('get', iq.getAttribute('type'))
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, items)
self.assertEquals(1, len(items))
self.assertEquals('moderator', items[0].getAttribute('role'))
response = toResponse(iq, 'result')
query = response.addElement((NS_MUC_ADMIN, 'query'))
item = query.addElement('item')
item['affiliation'] ='member'
item['jid'] = 'hag66@shakespeare.lit'
item['nick'] = 'thirdwitch'
item['role'] = 'moderator'
self.stub.send(response)
return d
def test_modifyAffiliationList(self):
entities = [JID('user1@test.example.org'),
JID('user2@test.example.org')]
d = self.protocol.modifyAffiliationList(self.roomJID, entities,
'admin')
iq = self.stub.output[-1]
query = "/iq/query[@xmlns='%s']/item[@xmlns='%s']" % (muc.NS_MUC_ADMIN,
muc.NS_MUC_ADMIN)
items = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, items)
self.assertEquals(entities[0], JID(items[0].getAttribute('jid')))
self.assertEquals('admin', items[0].getAttribute('affiliation'))
self.assertEquals(entities[1], JID(items[1].getAttribute('jid')))
self.assertEquals('admin', items[1].getAttribute('affiliation'))
# Send a response to have the deferred fire.
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_grantVoice(self):
"""
Granting voice sends request to set role to 'participant'.
"""
nick = 'TroubleMaker'
def cb(give_voice):
self.assertTrue(give_voice, 'Did not give voice user')
d = self.protocol.grantVoice(self.roomJID, nick,
sender=self.userJID)
d.addCallback(cb)
iq = self.stub.output[-1]
query = (u"/iq[@type='set' and @to='%s']/query/item"
"[@role='participant']") % self.roomJID
self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza')
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_revokeVoice(self):
"""
Revoking voice sends request to set role to 'visitor'.
"""
nick = 'TroubleMaker'
d = self.protocol.revokeVoice(self.roomJID, nick,
reason="Trouble maker",
sender=self.userJID)
iq = self.stub.output[-1]
query = (u"/iq[@type='set' and @to='%s']/query/item"
"[@role='visitor']") % self.roomJID
self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza')
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_grantModerator(self):
"""
Granting moderator privileges sends request to set role to 'moderator'.
"""
nick = 'TroubleMaker'
d = self.protocol.grantModerator(self.roomJID, nick,
sender=self.userJID)
iq = self.stub.output[-1]
query = (u"/iq[@type='set' and @to='%s']/query/item"
"[@role='moderator']") % self.roomJID
self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza')
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_ban(self):
"""
Ban an entity in a room.
"""
banned = JID('ban@jabber.org/TroubleMaker')
def cb(banned):
self.assertTrue(banned, 'Did not ban user')
d = self.protocol.ban(self.roomJID, banned, reason='Spam',
sender=self.userJID)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertTrue(xpath.matches(
u"/iq[@type='set' and @to='%s']/query/item"
"[@affiliation='outcast']" % (self.roomJID,),
iq),
'Wrong ban stanza')
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_kick(self):
"""
Kick an entity from a room.
"""
nick = 'TroubleMaker'
def cb(kicked):
self.assertTrue(kicked, 'Did not kick user')
d = self.protocol.kick(self.roomJID, nick, reason='Spam',
sender=self.userJID)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertTrue(xpath.matches(
u"/iq[@type='set' and @to='%s']/query/item"
"[@role='none']" % (self.roomJID,),
iq),
'Wrong kick stanza')
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_destroy(self):
"""
Destroy a room.
"""
d = self.protocol.destroy(self.occupantJID, reason='Time to leave',
alternate=JID('other@%s' % self.service),
password='secret')
iq = self.stub.output[-1]
query = ("/iq/query[@xmlns='%s']/destroy[@xmlns='%s']" %
(muc.NS_MUC_OWNER, muc.NS_MUC_OWNER))
nodes = xpath.queryForNodes(query, iq)
self.assertNotIdentical(None, nodes, 'Bad configure request')
destroy = nodes[0]
self.assertEquals('Time to leave', unicode(destroy.reason))
response = toResponse(iq, 'result')
self.stub.send(response)
return d
class MUCClientTest(unittest.TestCase):
"""
Tests for C{muc.MUCClient}.
"""
def setUp(self):
self.clock = task.Clock()
self.sessionManager = TestableStreamManager(reactor=self.clock)
self.stub = self.sessionManager.stub
self.protocol = muc.MUCClient(reactor=self.clock)
self.protocol.setHandlerParent(self.sessionManager)
self.roomIdentifier = 'test'
self.service = 'conference.example.org'
self.nick = 'Nick'
self.occupantJID = JID(tuple=(self.roomIdentifier,
self.service,
self.nick))
self.roomJID = self.occupantJID.userhostJID()
self.userJID = JID('test@example.org/Testing')
def _createRoom(self):
"""
A helper method to create a test room.
"""
# create a room
room = muc.Room(self.roomJID, self.nick)
self.protocol._addRoom(room)
return room
def test_interface(self):
"""
Do instances of L{muc.MUCClient} provide L{iwokkel.IMUCClient}?
"""
verify.verifyObject(iwokkel.IMUCClient, self.protocol)
def _testPresence(self, sender='', available=True):
"""
Helper for presence tests.
"""
def userUpdatedStatus(room, user, show, status):
self.fail("Unexpected call to userUpdatedStatus")
def userJoinedRoom(room, user):
self.fail("Unexpected call to userJoinedRoom")
if available:
available = ""
else:
available = " type='unavailable'"
if sender:
sender = u" from='%s'" % sender
xml = u"""
""" % (self.userJID, sender, available)
self.protocol.userUpdatedStatus = userUpdatedStatus
self.protocol.userJoinedRoom = userJoinedRoom
self.stub.send(parseXml(xml))
def test_availableReceivedEmptySender(self):
"""
Availability presence from empty sender is ignored.
"""
self._testPresence(sender='')
def test_availableReceivedNotInRoom(self):
"""
Availability presence from unknown entities is ignored.
"""
otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick')
self._testPresence(sender=otherOccupantJID)
def test_unavailableReceivedEmptySender(self):
"""
Availability presence from empty sender is ignored.
"""
self._testPresence(sender='', available=False)
def test_unavailableReceivedNotInRoom(self):
"""
Availability presence from unknown entities is ignored.
"""
otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick')
self._testPresence(sender=otherOccupantJID, available=False)
def test_unavailableReceivedNotInRoster(self):
"""
Availability presence from unknown entities is ignored.
"""
room = self._createRoom()
user = muc.User(self.nick)
room.addUser(user)
otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick')
self._testPresence(sender=otherOccupantJID, available=False)
def test_userJoinedRoom(self):
"""
Joins by others to a room we're in are passed to userJoinedRoom
"""
xml = """
""" % (self.userJID.full(), self.occupantJID.full())
# create a room
self._createRoom()
def userJoinedRoom(room, user):
self.assertEquals(self.roomJID, room.roomJID,
'Wrong room name')
self.assertTrue(room.inRoster(user), 'User not in roster')
d, self.protocol.userJoinedRoom = calledAsync(userJoinedRoom)
self.stub.send(parseXml(xml))
return d
def test_receivedSubject(self):
"""
Subject received from a room we're in are passed to receivedSubject.
"""
xml = u"""
test
""" % (self.userJID, self.occupantJID)
self._createRoom()
# add user to room
user = muc.User(self.nick)
room = self.protocol._getRoom(self.roomJID)
room.addUser(user)
def receivedSubject(room, user, subject):
self.assertEquals('test', subject, "Wrong group chat message")
self.assertEquals(self.roomJID, room.roomJID,
'Wrong room name')
self.assertEquals(self.nick, user.nick)
d, self.protocol.receivedSubject = calledAsync(receivedSubject)
self.stub.send(parseXml(xml))
return d
def test_receivedSubjectNotOverridden(self):
"""
Not overriding receivedSubject is ok.
"""
xml = u"""
test
""" % (self.userJID, self.occupantJID)
self._createRoom()
self.stub.send(parseXml(xml))
def test_receivedGroupChat(self):
"""
Messages received from a room we're in are passed to receivedGroupChat.
"""
xml = u"""
test
""" % (self.occupantJID)
self._createRoom()
def receivedGroupChat(room, user, message):
self.assertEquals('test', message.body, "Wrong group chat message")
self.assertEquals(self.roomJID, room.roomJID,
'Wrong room name')
d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat)
self.stub.send(parseXml(xml))
return d
def test_receivedGroupChatRoom(self):
"""
Messages received from the room itself have C{user} set to C{None}.
"""
xml = u"""
test
""" % (self.roomJID)
self._createRoom()
def receivedGroupChat(room, user, message):
self.assertIdentical(None, user)
d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat)
self.stub.send(parseXml(xml))
return d
def test_receivedGroupChatNotInRoom(self):
"""
Messages received from a room we're not in are ignored.
"""
xml = u"""
test
""" % (self.occupantJID)
def receivedGroupChat(room, user, message):
self.fail("Unexpected call to receivedGroupChat")
self.protocol.receivedGroupChat = receivedGroupChat
self.stub.send(parseXml(xml))
def test_receivedGroupChatNotOverridden(self):
"""
Not overriding receivedGroupChat is ok.
"""
xml = u"""
test
""" % (self.occupantJID)
self._createRoom()
self.stub.send(parseXml(xml))
def test_join(self):
"""
Joining a room waits for confirmation, deferred fires room.
"""
def cb(room):
self.assertEqual(self.roomJID, room.roomJID)
self.assertFalse(room.locked)
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(cb)
# send back user presence, they joined
xml = """
""" % (self.roomIdentifier, self.service, self.nick)
self.stub.send(parseXml(xml))
return d
def test_joinLocked(self):
"""
A new room is locked by default.
"""
def cb(room):
self.assertTrue(room.locked, "Room is not marked as locked")
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(cb)
# send back user presence, they joined
xml = """
""" % (self.roomIdentifier, self.service, self.nick)
self.stub.send(parseXml(xml))
return d
def test_joinForbidden(self):
"""
A forbidden error in response to a join errbacks with L{StanzaError}.
"""
def cb(error):
self.assertEquals('forbidden', error.condition,
'Wrong muc condition')
self.assertIdentical(None, self.protocol._getRoom(self.roomJID))
d = self.protocol.join(self.roomJID, self.nick)
self.assertFailure(d, StanzaError)
d.addCallback(cb)
# send back error, forbidden
xml = u"""
""" % (self.occupantJID)
self.stub.send(parseXml(xml))
return d
def test_userLeftRoom(self):
"""
Unavailable presence from a participant removes it from the room.
"""
xml = u"""
""" % (self.userJID, self.occupantJID)
# create a room
self._createRoom()
# add user to room
user = muc.User(self.nick)
room = self.protocol._getRoom(self.roomJID)
room.addUser(user)
def userLeftRoom(room, user):
self.assertEquals(self.roomJID, room.roomJID,
'Wrong room name')
self.assertFalse(room.inRoster(user), 'User in roster')
d, self.protocol.userLeftRoom = calledAsync(userLeftRoom)
self.stub.send(parseXml(xml))
return d
def test_receivedHistory(self):
"""
Receiving history on room join.
"""
xml = u"""
test
""" % (self.occupantJID, self.userJID)
self._createRoom()
def receivedHistory(room, user, message):
self.assertEquals('test', message.body, "wrong message body")
stamp = datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())
self.assertEquals(stamp, message.delay.stamp,
'Does not have a history stamp')
d, self.protocol.receivedHistory = calledAsync(receivedHistory)
self.stub.send(parseXml(xml))
return d
def test_receivedHistoryNotOverridden(self):
"""
Not overriding receivedHistory is ok.
"""
xml = u"""
test
""" % (self.occupantJID, self.userJID)
self._createRoom()
self.stub.send(parseXml(xml))
def test_nickConflict(self):
"""
If the server finds the new nick in conflict, the errback is called.
"""
def cb(failure, room):
user = room.getUser(otherNick)
self.assertNotIdentical(None, user)
self.assertEqual(otherJID, user.entity)
def joined(room):
d = self.protocol.nick(room.roomJID, otherNick)
self.assertFailure(d, StanzaError)
d.addCallback(cb, room)
otherJID = JID('other@example.org/Home')
otherNick = 'otherNick'
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(joined)
# Send back other partipant's presence.
xml = u"""
""" % (self.roomJID, otherNick, otherJID)
self.stub.send(parseXml(xml))
# send back user presence, they joined
xml = u"""
""" % (self.roomJID, self.nick)
self.stub.send(parseXml(xml))
# send back error presence, nick conflicted
xml = u"""
""" % (self.roomJID, otherNick)
self.stub.send(parseXml(xml))
return d
def test_nick(self):
"""
Send a nick change to the server.
"""
newNick = 'newNick'
room = self._createRoom()
def joined(room):
self.assertEqual(self.roomJID, room.roomJID)
self.assertEqual(newNick, room.nick)
user = room.getUser(newNick)
self.assertNotIdentical(None, user)
self.assertEqual(newNick, user.nick)
d = self.protocol.nick(self.roomJID, newNick)
d.addCallback(joined)
# Nick should not have been changed, yet, as we haven't gotten
# confirmation, yet.
self.assertEquals(self.nick, room.nick)
# send back user presence, nick changed
xml = u"""
""" % (self.roomJID, newNick)
self.stub.send(parseXml(xml))
return d
def test_leave(self):
"""
Client leaves a room
"""
def joined(_):
return self.protocol.leave(self.roomJID)
def left(_):
self.assertIdentical(None, self.protocol._getRoom(self.roomJID))
# Join the room
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(joined)
d.addCallback(left)
# Receive presence back from the room: joined.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
# Receive presence back from the room: left.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
return d
def test_status(self):
"""
Change status
"""
def joined(_):
d = self.protocol.status(self.roomJID, 'xa', 'testing MUC')
d.addCallback(statusChanged)
return d
def statusChanged(room):
self.assertEqual(self.roomJID, room.roomJID)
user = room.getUser(self.nick)
self.assertNotIdentical(None, user, 'User not found')
self.assertEqual('testing MUC', user.status, 'Wrong status')
self.assertEqual('xa', user.show, 'Wrong show')
# Join the room
d = self.protocol.join(self.roomJID, self.nick)
d.addCallback(joined)
# Receive presence back from the room: joined.
xml = u"""
""" % (self.userJID, self.occupantJID)
self.stub.send(parseXml(xml))
# send back user presence, status changed
xml = u"""
xa
testing MUC
""" % self.occupantJID
self.stub.send(parseXml(xml))
return d
def test_destroy(self):
"""
Destroy a room.
"""
def destroyed(_):
self.assertIdentical(None, self.protocol._getRoom(self.roomJID))
d = self.protocol.destroy(self.occupantJID, reason='Time to leave',
alternate=JID('other@%s' % self.service),
password='secret')
d.addCallback(destroyed)
iq = self.stub.output[-1]
response = toResponse(iq, 'result')
self.stub.send(response)
return d
wokkel-0.7.1/wokkel/test/test_disco.py 0000775 0001750 0001750 00000075625 11734574117 020626 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.disco}.
"""
from zope.interface import implements
from twisted.internet import defer
from twisted.trial import unittest
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import toResponse
from twisted.words.xish import domish, utility
from wokkel import data_form, disco
from wokkel.generic import parseXml
from wokkel.subprotocols import XMPPHandler
from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
NS_DISCO_INFO = 'http://jabber.org/protocol/disco#info'
NS_DISCO_ITEMS = 'http://jabber.org/protocol/disco#items'
class DiscoFeatureTest(unittest.TestCase):
"""
Tests for L{disco.DiscoFeature}.
"""
def test_init(self):
"""
Test initialization with a with feature namespace URI.
"""
feature = disco.DiscoFeature(u'testns')
self.assertEqual(u'testns', feature)
def test_toElement(self):
"""
Test proper rendering to a DOM representation.
The returned element should be properly named and have a C{var}
attribute that holds the feature namespace URI.
"""
feature = disco.DiscoFeature(u'testns')
element = feature.toElement()
self.assertEqual(NS_DISCO_INFO, element.uri)
self.assertEqual(u'feature', element.name)
self.assertTrue(element.hasAttribute(u'var'))
self.assertEqual(u'testns', element[u'var'])
def test_fromElement(self):
"""
Test creating L{disco.DiscoFeature} from L{domish.Element}.
"""
element = domish.Element((NS_DISCO_INFO, u'feature'))
element['var'] = u'testns'
feature = disco.DiscoFeature.fromElement(element)
self.assertEqual(u'testns', feature)
class DiscoIdentityTest(unittest.TestCase):
"""
Tests for L{disco.DiscoIdentity}.
"""
def test_init(self):
"""
Test initialization with a category, type and name.
"""
identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom')
self.assertEqual(u'conference', identity.category)
self.assertEqual(u'text', identity.type)
self.assertEqual(u'The chatroom', identity.name)
def test_toElement(self):
"""
Test proper rendering to a DOM representation.
The returned element should be properly named and have C{conference},
C{type}, and C{name} attributes.
"""
identity = disco.DiscoIdentity(u'conference', u'text', u'The chatroom')
element = identity.toElement()
self.assertEqual(NS_DISCO_INFO, element.uri)
self.assertEqual(u'identity', element.name)
self.assertEqual(u'conference', element.getAttribute(u'category'))
self.assertEqual(u'text', element.getAttribute(u'type'))
self.assertEqual(u'The chatroom', element.getAttribute(u'name'))
def test_toElementWithoutName(self):
"""
Test proper rendering to a DOM representation without a name.
The returned element should be properly named and have C{conference},
C{type} attributes, no C{name} attribute.
"""
identity = disco.DiscoIdentity(u'conference', u'text')
element = identity.toElement()
self.assertEqual(NS_DISCO_INFO, element.uri)
self.assertEqual(u'identity', element.name)
self.assertEqual(u'conference', element.getAttribute(u'category'))
self.assertEqual(u'text', element.getAttribute(u'type'))
self.assertFalse(element.hasAttribute(u'name'))
def test_fromElement(self):
"""
Test creating L{disco.DiscoIdentity} from L{domish.Element}.
"""
element = domish.Element((NS_DISCO_INFO, u'identity'))
element['category'] = u'conference'
element['type'] = u'text'
element['name'] = u'The chatroom'
identity = disco.DiscoIdentity.fromElement(element)
self.assertEqual(u'conference', identity.category)
self.assertEqual(u'text', identity.type)
self.assertEqual(u'The chatroom', identity.name)
def test_fromElementWithoutName(self):
"""
Test creating L{disco.DiscoIdentity} from L{domish.Element}, no name.
"""
element = domish.Element((NS_DISCO_INFO, u'identity'))
element['category'] = u'conference'
element['type'] = u'text'
identity = disco.DiscoIdentity.fromElement(element)
self.assertEqual(u'conference', identity.category)
self.assertEqual(u'text', identity.type)
self.assertEqual(None, identity.name)
class DiscoInfoTest(unittest.TestCase):
"""
Tests for L{disco.DiscoInfo}.
"""
def test_toElement(self):
"""
Test C{toElement} creates a correctly namespaced element, no node.
"""
info = disco.DiscoInfo()
element = info.toElement()
self.assertEqual(NS_DISCO_INFO, element.uri)
self.assertEqual(u'query', element.name)
self.assertFalse(element.hasAttribute(u'node'))
def test_toElementNode(self):
"""
Test C{toElement} with a node.
"""
info = disco.DiscoInfo()
info.nodeIdentifier = u'test'
element = info.toElement()
self.assertEqual(u'test', element.getAttribute(u'node'))
def test_toElementChildren(self):
"""
Test C{toElement} creates a DOM with proper childs.
"""
info = disco.DiscoInfo()
info.append(disco.DiscoFeature(u'jabber:iq:register'))
info.append(disco.DiscoIdentity(u'conference', u'text'))
info.append(data_form.Form(u'result'))
element = info.toElement()
featureElements = domish.generateElementsQNamed(element.children,
u'feature',
NS_DISCO_INFO)
self.assertEqual(1, len(list(featureElements)))
identityElements = domish.generateElementsQNamed(element.children,
u'identity',
NS_DISCO_INFO)
self.assertEqual(1, len(list(identityElements)))
extensionElements = domish.generateElementsQNamed(element.children,
u'x',
data_form.NS_X_DATA)
self.assertEqual(1, len(list(extensionElements)))
def test_fromElement(self):
"""
Test properties when creating L{disco.DiscoInfo} from L{domish.Element}.
"""
xml = """
http://jabber.org/protocol/muc#roominfo
"""
element = parseXml(xml)
info = disco.DiscoInfo.fromElement(element)
self.assertIn(u'http://jabber.org/protocol/muc', info.features)
self.assertIn(u'jabber:iq:register', info.features)
self.assertIn((u'conference', u'text'), info.identities)
self.assertEqual(u'A Dark Cave',
info.identities[(u'conference', u'text')])
self.assertIn(u'http://jabber.org/protocol/muc#roominfo',
info.extensions)
def test_fromElementItems(self):
"""
Test items when creating L{disco.DiscoInfo} from L{domish.Element}.
"""
xml = """
http://jabber.org/protocol/muc#roominfo
"""
element = parseXml(xml)
info = disco.DiscoInfo.fromElement(element)
info = list(info)
self.assertEqual(4, len(info))
identity = info[0]
self.assertEqual(u'conference', identity.category)
self.assertEqual(u'http://jabber.org/protocol/muc', info[1])
self.assertEqual(u'jabber:iq:register', info[2])
extension = info[3]
self.assertEqual(u'http://jabber.org/protocol/muc#roominfo',
extension.formNamespace)
def test_fromElementNoNode(self):
"""
Test creating L{disco.DiscoInfo} from L{domish.Element}, no node.
"""
xml = """"""
element = parseXml(xml)
info = disco.DiscoInfo.fromElement(element)
self.assertEqual(u'', info.nodeIdentifier)
def test_fromElementNode(self):
"""
Test creating L{disco.DiscoInfo} from L{domish.Element}, with node.
"""
xml = """
"""
element = parseXml(xml)
info = disco.DiscoInfo.fromElement(element)
self.assertEqual(u'test', info.nodeIdentifier)
class DiscoItemTest(unittest.TestCase):
"""
Tests for L{disco.DiscoItem}.
"""
def test_init(self):
"""
Test initialization with a category, type and name.
"""
item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node')
self.assertEqual(JID(u'example.org'), item.entity)
self.assertEqual(u'test', item.nodeIdentifier)
self.assertEqual(u'The node', item.name)
def test_toElement(self):
"""
Test proper rendering to a DOM representation.
The returned element should be properly named and have C{jid}, C{node},
and C{name} attributes.
"""
item = disco.DiscoItem(JID(u'example.org'), u'test', u'The node')
element = item.toElement()
self.assertEqual(NS_DISCO_ITEMS, element.uri)
self.assertEqual(u'item', element.name)
self.assertEqual(u'example.org', element.getAttribute(u'jid'))
self.assertEqual(u'test', element.getAttribute(u'node'))
self.assertEqual(u'The node', element.getAttribute(u'name'))
def test_toElementWithoutName(self):
"""
Test proper rendering to a DOM representation without a name.
The returned element should be properly named and have C{jid}, C{node}
attributes, no C{name} attribute.
"""
item = disco.DiscoItem(JID(u'example.org'), u'test')
element = item.toElement()
self.assertEqual(NS_DISCO_ITEMS, element.uri)
self.assertEqual(u'item', element.name)
self.assertEqual(u'example.org', element.getAttribute(u'jid'))
self.assertEqual(u'test', element.getAttribute(u'node'))
self.assertFalse(element.hasAttribute(u'name'))
def test_fromElement(self):
"""
Test creating L{disco.DiscoItem} from L{domish.Element}.
"""
element = domish.Element((NS_DISCO_ITEMS, u'item'))
element[u'jid'] = u'example.org'
element[u'node'] = u'test'
element[u'name'] = u'The node'
item = disco.DiscoItem.fromElement(element)
self.assertEqual(JID(u'example.org'), item.entity)
self.assertEqual(u'test', item.nodeIdentifier)
self.assertEqual(u'The node', item.name)
def test_fromElementNoNode(self):
"""
Test creating L{disco.DiscoItem} from L{domish.Element}, no node.
"""
element = domish.Element((NS_DISCO_ITEMS, u'item'))
element[u'jid'] = u'example.org'
element[u'name'] = u'The node'
item = disco.DiscoItem.fromElement(element)
self.assertEqual(JID(u'example.org'), item.entity)
self.assertEqual(u'', item.nodeIdentifier)
self.assertEqual(u'The node', item.name)
def test_fromElementNoName(self):
"""
Test creating L{disco.DiscoItem} from L{domish.Element}, no name.
"""
element = domish.Element((NS_DISCO_ITEMS, u'item'))
element[u'jid'] = u'example.org'
element[u'node'] = u'test'
item = disco.DiscoItem.fromElement(element)
self.assertEqual(JID(u'example.org'), item.entity)
self.assertEqual(u'test', item.nodeIdentifier)
self.assertEqual(None, item.name)
def test_fromElementBadJID(self):
"""
Test creating L{disco.DiscoItem} from L{domish.Element}, bad JID.
"""
element = domish.Element((NS_DISCO_ITEMS, u'item'))
element[u'jid'] = u'ex@@@ample.org'
item = disco.DiscoItem.fromElement(element)
self.assertIdentical(None, item.entity)
class DiscoItemsTest(unittest.TestCase):
"""
Tests for L{disco.DiscoItems}.
"""
def test_toElement(self):
"""
Test C{toElement} creates a correctly namespaced element, no node.
"""
items = disco.DiscoItems()
element = items.toElement()
self.assertEqual(NS_DISCO_ITEMS, element.uri)
self.assertEqual(u'query', element.name)
self.assertFalse(element.hasAttribute(u'node'))
def test_toElementNode(self):
"""
Test C{toElement} with a node.
"""
items = disco.DiscoItems()
items.nodeIdentifier = u'test'
element = items.toElement()
self.assertEqual(u'test', element.getAttribute(u'node'))
def test_toElementChildren(self):
"""
Test C{toElement} creates a DOM with proper childs.
"""
items = disco.DiscoItems()
items.append(disco.DiscoItem(JID(u'example.org'), u'test', u'A node'))
element = items.toElement()
itemElements = domish.generateElementsQNamed(element.children,
u'item',
NS_DISCO_ITEMS)
self.assertEqual(1, len(list(itemElements)))
def test_fromElement(self):
"""
Test creating L{disco.DiscoItems} from L{domish.Element}.
"""
xml = """
"""
element = parseXml(xml)
items = disco.DiscoItems.fromElement(element)
items = list(items)
self.assertEqual(1, len(items))
item = items[0]
self.assertEqual(JID(u'example.org'), item.entity)
self.assertEqual(u'test', item.nodeIdentifier)
self.assertEqual(u'A node', item.name)
def test_fromElementNoNode(self):
"""
Test creating L{disco.DiscoItems} from L{domish.Element}, no node.
"""
xml = """"""
element = parseXml(xml)
items = disco.DiscoItems.fromElement(element)
self.assertEqual(u'', items.nodeIdentifier)
def test_fromElementNode(self):
"""
Test creating L{disco.DiscoItems} from L{domish.Element}, with node.
"""
xml = """
"""
element = parseXml(xml)
items = disco.DiscoItems.fromElement(element)
self.assertEqual(u'test', items.nodeIdentifier)
class DiscoClientProtocolTest(unittest.TestCase):
"""
Tests for L{disco.DiscoClientProtocol}.
"""
def setUp(self):
"""
Set up stub and protocol for testing.
"""
self.stub = XmlStreamStub()
self.patch(XMPPHandler, 'request', self.request)
self.protocol = disco.DiscoClientProtocol()
def request(self, request):
element = request.toElement()
self.stub.xmlstream.send(element)
return defer.Deferred()
def test_requestItems(self):
"""
Test request sent out by C{requestItems} and parsing of response.
"""
def cb(items):
items = list(items)
self.assertEqual(2, len(items))
self.assertEqual(JID(u'test.example.org'), items[0].entity)
d = self.protocol.requestItems(JID(u'example.org'),u"foo")
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEqual(u'example.org', iq.getAttribute(u'to'))
self.assertEqual(u'get', iq.getAttribute(u'type'))
self.assertEqual(u'foo', iq.query.getAttribute(u'node'))
self.assertEqual(NS_DISCO_ITEMS, iq.query.uri)
response = toResponse(iq, u'result')
query = response.addElement((NS_DISCO_ITEMS, u'query'))
element = query.addElement(u'item')
element[u'jid'] = u'test.example.org'
element[u'node'] = u'music'
element[u'name'] = u'Music from the time of Shakespeare'
element = query.addElement(u'item')
element[u'jid'] = u"test2.example.org"
d.callback(response)
return d
def test_requestItemsFrom(self):
"""
A disco items request can be sent with an explicit sender address.
"""
d = self.protocol.requestItems(JID(u'example.org'),
sender=JID(u'test.example.org'))
iq = self.stub.output[-1]
self.assertEqual(u'test.example.org', iq.getAttribute(u'from'))
response = toResponse(iq, u'result')
response.addElement((NS_DISCO_ITEMS, u'query'))
d.callback(response)
return d
def test_requestInfo(self):
"""
Test request sent out by C{requestInfo} and parsing of response.
"""
def cb(info):
self.assertIn((u'conference', u'text'), info.identities)
self.assertIn(u'http://jabber.org/protocol/disco#info',
info.features)
self.assertIn(u'http://jabber.org/protocol/muc',
info.features)
d = self.protocol.requestInfo(JID(u'example.org'),'foo')
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEqual(u'example.org', iq.getAttribute(u'to'))
self.assertEqual(u'get', iq.getAttribute(u'type'))
self.assertEqual(u'foo', iq.query.getAttribute(u'node'))
self.assertEqual(NS_DISCO_INFO, iq.query.uri)
response = toResponse(iq, u'result')
query = response.addElement((NS_DISCO_INFO, u'query'))
element = query.addElement(u"identity")
element[u'category'] = u'conference' # required
element[u'type'] = u'text' # required
element[u"name"] = u'Romeo and Juliet, Act II, Scene II' # optional
element = query.addElement("feature")
element[u'var'] = u'http://jabber.org/protocol/disco#info' # required
element = query.addElement(u"feature")
element[u'var'] = u'http://jabber.org/protocol/muc'
d.callback(response)
return d
def test_requestInfoFrom(self):
"""
A disco info request can be sent with an explicit sender address.
"""
d = self.protocol.requestInfo(JID(u'example.org'),
sender=JID(u'test.example.org'))
iq = self.stub.output[-1]
self.assertEqual(u'test.example.org', iq.getAttribute(u'from'))
response = toResponse(iq, u'result')
response.addElement((NS_DISCO_INFO, u'query'))
d.callback(response)
return d
class DiscoHandlerTest(unittest.TestCase, TestableRequestHandlerMixin):
"""
Tests for L{disco.DiscoHandler}.
"""
def setUp(self):
self.service = disco.DiscoHandler()
def test_connectionInitializedObserveInfo(self):
"""
An observer for Disco Info requests is setup on stream initialization.
"""
xml = """
""" % NS_DISCO_INFO
def handleRequest(iq):
called.append(iq)
called = []
self.service.xmlstream = utility.EventDispatcher()
self.service.handleRequest = handleRequest
self.service.connectionInitialized()
self.service.xmlstream.dispatch(parseXml(xml))
self.assertEqual(1, len(called))
def test_connectionInitializedObserveItems(self):
"""
An observer for Disco Items requests is setup on stream initialization.
"""
xml = """
""" % NS_DISCO_ITEMS
def handleRequest(iq):
called.append(iq)
called = []
self.service.xmlstream = utility.EventDispatcher()
self.service.handleRequest = handleRequest
self.service.connectionInitialized()
self.service.xmlstream.dispatch(parseXml(xml))
self.assertEqual(1, len(called))
def test_onDiscoInfo(self):
"""
C{onDiscoInfo} should process an info request and return a response.
The request should be parsed, C{info} called with the extracted
parameters, and then the result should be formatted into a proper
response element.
"""
xml = """
""" % NS_DISCO_INFO
def cb(element):
self.assertEqual('query', element.name)
self.assertEqual(NS_DISCO_INFO, element.uri)
self.assertEqual(NS_DISCO_INFO, element.identity.uri)
self.assertEqual('dummy', element.identity['category'])
self.assertEqual('generic', element.identity['type'])
self.assertEqual('Generic Dummy Entity', element.identity['name'])
self.assertEqual(NS_DISCO_INFO, element.feature.uri)
self.assertEqual('jabber:iq:version', element.feature['var'])
def info(requestor, target, nodeIdentifier):
self.assertEqual(JID('test@example.com'), requestor)
self.assertEqual(JID('example.com'), target)
self.assertEqual('', nodeIdentifier)
return defer.succeed([
disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
disco.DiscoFeature('jabber:iq:version')
])
self.service.info = info
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_onDiscoInfoWithNoFromAttribute(self):
"""
Disco info request without a from attribute has requestor None.
"""
xml = """
""" % NS_DISCO_INFO
def info(requestor, target, nodeIdentifier):
self.assertEqual(None, requestor)
return defer.succeed([
disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
disco.DiscoFeature('jabber:iq:version')
])
self.service.info = info
d = self.handleRequest(xml)
return d
def test_onDiscoInfoWithNoToAttribute(self):
"""
Disco info request without a to attribute has target None.
"""
xml = """
""" % NS_DISCO_INFO
def info(requestor, target, nodeIdentifier):
self.assertEqual(JID('test@example.com'), requestor)
return defer.succeed([
disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
disco.DiscoFeature('jabber:iq:version')
])
self.service.info = info
d = self.handleRequest(xml)
return d
def test_onDiscoInfoWithNode(self):
"""
An info request for a node should return it in the response.
"""
xml = """
""" % NS_DISCO_INFO
def cb(element):
self.assertTrue(element.hasAttribute('node'))
self.assertEqual('test', element['node'])
def info(requestor, target, nodeIdentifier):
self.assertEqual('test', nodeIdentifier)
return defer.succeed([
disco.DiscoFeature('jabber:iq:version')
])
self.service.info = info
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_onDiscoInfoWithNodeNoResults(self):
"""
An info request for a node with no results returns items-not-found.
"""
xml = """
""" % NS_DISCO_INFO
def cb(exc):
self.assertEquals('item-not-found', exc.condition)
def info(requestor, target, nodeIdentifier):
self.assertEqual('test', nodeIdentifier)
return defer.succeed([])
self.service.info = info
d = self.handleRequest(xml)
self.assertFailure(d, StanzaError)
d.addCallback(cb)
return d
def test_onDiscoItems(self):
"""
C{onDiscoItems} should process an items request and return a response.
The request should be parsed, C{items} called with the extracted
parameters, and then the result should be formatted into a proper
response element.
"""
xml = """
""" % NS_DISCO_ITEMS
def cb(element):
self.assertEqual('query', element.name)
self.assertEqual(NS_DISCO_ITEMS, element.uri)
self.assertEqual(NS_DISCO_ITEMS, element.item.uri)
self.assertEqual('example.com', element.item['jid'])
self.assertEqual('test', element.item['node'])
self.assertEqual('Test node', element.item['name'])
def items(requestor, target, nodeIdentifier):
self.assertEqual(JID('test@example.com'), requestor)
self.assertEqual(JID('example.com'), target)
self.assertEqual('', nodeIdentifier)
return defer.succeed([
disco.DiscoItem(JID('example.com'), 'test', 'Test node'),
])
self.service.items = items
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_onDiscoItemsWithNode(self):
"""
An items request for a node should return it in the response.
"""
xml = """
""" % NS_DISCO_ITEMS
def cb(element):
self.assertTrue(element.hasAttribute('node'))
self.assertEqual('test', element['node'])
def items(requestor, target, nodeIdentifier):
self.assertEqual('test', nodeIdentifier)
return defer.succeed([
disco.DiscoFeature('jabber:iq:version')
])
self.service.items = items
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_info(self):
"""
C{info} should gather disco info from sibling handlers.
"""
discoItems = [disco.DiscoIdentity('dummy', 'generic',
'Generic Dummy Entity'),
disco.DiscoFeature('jabber:iq:version')
]
class DiscoResponder(XMPPHandler):
implements(disco.IDisco)
def getDiscoInfo(self, requestor, target, nodeIdentifier):
if not nodeIdentifier:
return defer.succeed(discoItems)
else:
return defer.succeed([])
def cb(result):
self.assertEquals(discoItems, result)
self.service.parent = [self.service, DiscoResponder()]
d = self.service.info(JID('test@example.com'), JID('example.com'), '')
d.addCallback(cb)
return d
def test_infoNotDeferred(self):
"""
C{info} should gather disco info from sibling handlers.
"""
discoItems = [disco.DiscoIdentity('dummy', 'generic',
'Generic Dummy Entity'),
disco.DiscoFeature('jabber:iq:version')
]
class DiscoResponder(XMPPHandler):
implements(disco.IDisco)
def getDiscoInfo(self, requestor, target, nodeIdentifier):
if not nodeIdentifier:
return discoItems
else:
return []
def cb(result):
self.assertEquals(discoItems, result)
self.service.parent = [self.service, DiscoResponder()]
d = self.service.info(JID('test@example.com'), JID('example.com'), '')
d.addCallback(cb)
return d
def test_items(self):
"""
C{info} should gather disco items from sibling handlers.
"""
discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')]
class DiscoResponder(XMPPHandler):
implements(disco.IDisco)
def getDiscoItems(self, requestor, target, nodeIdentifier):
if not nodeIdentifier:
return defer.succeed(discoItems)
else:
return defer.succeed([])
def cb(result):
self.assertEquals(discoItems, result)
self.service.parent = [self.service, DiscoResponder()]
d = self.service.items(JID('test@example.com'), JID('example.com'), '')
d.addCallback(cb)
return d
def test_itemsNotDeferred(self):
"""
C{info} should also collect results not returned via a deferred.
"""
discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')]
class DiscoResponder(XMPPHandler):
implements(disco.IDisco)
def getDiscoItems(self, requestor, target, nodeIdentifier):
if not nodeIdentifier:
return discoItems
else:
return []
def cb(result):
self.assertEquals(discoItems, result)
self.service.parent = [self.service, DiscoResponder()]
d = self.service.items(JID('test@example.com'), JID('example.com'), '')
d.addCallback(cb)
return d
wokkel-0.7.1/wokkel/test/test_client.py 0000775 0001750 0001750 00000011715 12074266376 020774 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.client}.
"""
from twisted.internet import defer
from twisted.trial import unittest
from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber.client import XMPPAuthenticator
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT
from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from wokkel import client
class XMPPClientTest(unittest.TestCase):
"""
Tests for L{client.XMPPClient}.
"""
def setUp(self):
self.client = client.XMPPClient(JID('user@example.org'), 'secret')
def test_jid(self):
"""
Make sure the JID we pass is stored on the client.
"""
self.assertEquals(JID('user@example.org'), self.client.jid)
def test_jidWhenInitialized(self):
"""
Make sure that upon login, the JID is updated from the authenticator.
"""
xs = self.client.factory.buildProtocol(None)
self.client.factory.authenticator.jid = JID('user@example.org/test')
xs.dispatch(xs, xmlstream.STREAM_AUTHD_EVENT)
self.assertEquals(JID('user@example.org/test'), self.client.jid)
def test_domain(self):
"""
The domain to connect to is a byte string derived from the JID host.
"""
self.assertIsInstance(self.client.domain, bytes)
self.assertEqual(b'example.org', self.client.domain)
class DeferredClientFactoryTest(unittest.TestCase):
"""
Tests for L{client.DeferredClientFactory}.
"""
def setUp(self):
self.factory = client.DeferredClientFactory(JID('user@example.org'),
'secret')
def test_buildProtocol(self):
"""
The authenticator factory should be passed to its protocol and it
should instantiate the authenticator and save it.
L{xmlstream.XmlStream}s do that, but we also want to ensure it really
is one.
"""
xs = self.factory.buildProtocol(None)
self.assertIdentical(self.factory, xs.factory)
self.assertIsInstance(xs, xmlstream.XmlStream)
self.assertIsInstance(xs.authenticator, XMPPAuthenticator)
def test_deferredOnInitialized(self):
"""
Test the factory's deferred on stream initialization.
"""
xs = self.factory.buildProtocol(None)
xs.dispatch(xs, STREAM_AUTHD_EVENT)
return self.factory.deferred
def test_deferredOnNotInitialized(self):
"""
Test the factory's deferred on failed stream initialization.
"""
class TestException(Exception):
pass
xs = self.factory.buildProtocol(None)
xs.dispatch(TestException(), INIT_FAILED_EVENT)
self.assertFailure(self.factory.deferred, TestException)
return self.factory.deferred
def test_deferredOnConnectionFailure(self):
"""
Test the factory's deferred on connection faulure.
"""
class TestException(Exception):
pass
self.factory.buildProtocol(None)
self.factory.clientConnectionFailed(self, TestException())
self.assertFailure(self.factory.deferred, TestException)
return self.factory.deferred
def test_addHandler(self):
"""
Test the addition of a protocol handler.
"""
handler = XMPPHandler()
handler.setHandlerParent(self.factory.streamManager)
self.assertIn(handler, self.factory.streamManager)
self.assertIdentical(self.factory.streamManager, handler.parent)
def test_removeHandler(self):
"""
Test removal of a protocol handler.
"""
handler = XMPPHandler()
handler.setHandlerParent(self.factory.streamManager)
handler.disownHandlerParent(self.factory.streamManager)
self.assertNotIn(handler, self.factory.streamManager)
self.assertIdentical(None, handler.parent)
class ClientCreatorTest(unittest.TestCase):
"""
Tests for L{client.clientCreator}.
"""
def test_call(self):
"""
The factory is passed to an SRVConnector and a connection initiated.
"""
d1 = defer.Deferred()
factory = client.DeferredClientFactory(JID('user@example.org'),
'secret')
def cb(connector):
self.assertEqual('xmpp-client', connector.service)
self.assertIsInstance(connector.domain, bytes)
self.assertEqual(b'example.org', connector.domain)
self.assertEqual(factory, connector.factory)
def connect(connector):
d1.callback(connector)
d1.addCallback(cb)
self.patch(client.SRVConnector, 'connect', connect)
d2 = client.clientCreator(factory)
self.assertEqual(factory.deferred, d2)
return d1
wokkel-0.7.1/wokkel/test/test_pubsub.py 0000775 0001750 0001750 00000426572 12014253400 021003 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.pubsub}
"""
from zope.interface import verify
from twisted.trial import unittest
from twisted.internet import defer
from twisted.words.xish import domish
from twisted.words.protocols.jabber import error
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import toResponse
from wokkel import data_form, disco, iwokkel, pubsub, shim
from wokkel.generic import parseXml
from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
NS_PUBSUB_NODE_CONFIG = 'http://jabber.org/protocol/pubsub#node_config'
NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors'
NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner'
NS_PUBSUB_META_DATA = 'http://jabber.org/protocol/pubsub#meta-data'
NS_PUBSUB_SUBSCRIBE_OPTIONS = 'http://jabber.org/protocol/pubsub#subscribe_options'
def calledAsync(fn):
"""
Function wrapper that fires a deferred upon calling the given function.
"""
d = defer.Deferred()
def func(*args, **kwargs):
try:
result = fn(*args, **kwargs)
except:
d.errback()
else:
d.callback(result)
return d, func
class SubscriptionTest(unittest.TestCase):
"""
Tests for L{pubsub.Subscription}.
"""
def test_fromElement(self):
"""
fromElement parses a subscription from XML DOM.
"""
xml = """
"""
subscription = pubsub.Subscription.fromElement(parseXml(xml))
self.assertEqual('test', subscription.nodeIdentifier)
self.assertEqual(JID('user@example.org/Home'), subscription.subscriber)
self.assertEqual('pending', subscription.state)
self.assertIdentical(None, subscription.subscriptionIdentifier)
def test_fromElementWithSubscriptionIdentifier(self):
"""
A subscription identifier in the subscription should be parsed, too.
"""
xml = """
"""
subscription = pubsub.Subscription.fromElement(parseXml(xml))
self.assertEqual('1234', subscription.subscriptionIdentifier)
def test_toElement(self):
"""
Rendering a Subscription should yield the proper attributes.
"""
subscription = pubsub.Subscription('test',
JID('user@example.org/Home'),
'pending')
element = subscription.toElement()
self.assertEqual('subscription', element.name)
self.assertEqual(None, element.uri)
self.assertEqual('test', element.getAttribute('node'))
self.assertEqual('user@example.org/Home', element.getAttribute('jid'))
self.assertEqual('pending', element.getAttribute('subscription'))
self.assertFalse(element.hasAttribute('subid'))
def test_toElementEmptyNodeIdentifier(self):
"""
The empty node identifier should not yield a node attribute.
"""
subscription = pubsub.Subscription('',
JID('user@example.org/Home'),
'pending')
element = subscription.toElement()
self.assertFalse(element.hasAttribute('node'))
def test_toElementWithSubscriptionIdentifier(self):
"""
The subscription identifier, if set, is in the subid attribute.
"""
subscription = pubsub.Subscription('test',
JID('user@example.org/Home'),
'pending',
subscriptionIdentifier='1234')
element = subscription.toElement()
self.assertEqual('1234', element.getAttribute('subid'))
class PubSubClientTest(unittest.TestCase):
timeout = 2
def setUp(self):
self.stub = XmlStreamStub()
self.protocol = pubsub.PubSubClient()
self.protocol.xmlstream = self.stub.xmlstream
self.protocol.connectionInitialized()
def test_interface(self):
"""
Do instances of L{pubsub.PubSubClient} provide L{iwokkel.IPubSubClient}?
"""
verify.verifyObject(iwokkel.IPubSubClient, self.protocol)
def test_eventItems(self):
"""
Test receiving an items event resulting in a call to itemsReceived.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
items = event.addElement('items')
items['node'] = 'test'
item1 = items.addElement('item')
item1['id'] = 'item1'
item2 = items.addElement('retract')
item2['id'] = 'item2'
item3 = items.addElement('item')
item3['id'] = 'item3'
def itemsReceived(event):
self.assertEquals(JID('user@example.org/home'), event.recipient)
self.assertEquals(JID('pubsub.example.org'), event.sender)
self.assertEquals('test', event.nodeIdentifier)
self.assertEquals([item1, item2, item3], event.items)
d, self.protocol.itemsReceived = calledAsync(itemsReceived)
self.stub.send(message)
return d
def test_eventItemsCollection(self):
"""
Test receiving an items event resulting in a call to itemsReceived.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
items = event.addElement('items')
items['node'] = 'test'
headers = shim.Headers([('Collection', 'collection')])
message.addChild(headers)
def itemsReceived(event):
self.assertEquals(JID('user@example.org/home'), event.recipient)
self.assertEquals(JID('pubsub.example.org'), event.sender)
self.assertEquals('test', event.nodeIdentifier)
self.assertEquals({'Collection': ['collection']}, event.headers)
d, self.protocol.itemsReceived = calledAsync(itemsReceived)
self.stub.send(message)
return d
def test_eventItemsError(self):
"""
An error message with embedded event should not be handled.
This test uses an items event, which should not result in itemsReceived
being called. In general message.handled should be False.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
message['type'] = 'error'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
items = event.addElement('items')
items['node'] = 'test'
class UnexpectedCall(Exception):
pass
def itemsReceived(event):
raise UnexpectedCall("Unexpected call to itemsReceived")
self.protocol.itemsReceived = itemsReceived
self.stub.send(message)
self.assertFalse(message.handled)
def test_eventDelete(self):
"""
Test receiving a delete event resulting in a call to deleteReceived.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
delete = event.addElement('delete')
delete['node'] = 'test'
def deleteReceived(event):
self.assertEquals(JID('user@example.org/home'), event.recipient)
self.assertEquals(JID('pubsub.example.org'), event.sender)
self.assertEquals('test', event.nodeIdentifier)
d, self.protocol.deleteReceived = calledAsync(deleteReceived)
self.stub.send(message)
return d
def test_eventDeleteRedirect(self):
"""
Test receiving a delete event with a redirect URI.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
delete = event.addElement('delete')
delete['node'] = 'test'
uri = 'xmpp:pubsub.example.org?;node=test2'
delete.addElement('redirect')['uri'] = uri
def deleteReceived(event):
self.assertEquals(JID('user@example.org/home'), event.recipient)
self.assertEquals(JID('pubsub.example.org'), event.sender)
self.assertEquals('test', event.nodeIdentifier)
self.assertEquals(uri, event.redirectURI)
d, self.protocol.deleteReceived = calledAsync(deleteReceived)
self.stub.send(message)
return d
def test_event_purge(self):
"""
Test receiving a purge event resulting in a call to purgeReceived.
"""
message = domish.Element((None, 'message'))
message['from'] = 'pubsub.example.org'
message['to'] = 'user@example.org/home'
event = message.addElement((NS_PUBSUB_EVENT, 'event'))
items = event.addElement('purge')
items['node'] = 'test'
def purgeReceived(event):
self.assertEquals(JID('user@example.org/home'), event.recipient)
self.assertEquals(JID('pubsub.example.org'), event.sender)
self.assertEquals('test', event.nodeIdentifier)
d, self.protocol.purgeReceived = calledAsync(purgeReceived)
self.stub.send(message)
return d
def test_createNode(self):
"""
Test sending create request.
"""
def cb(nodeIdentifier):
self.assertEquals('test', nodeIdentifier)
d = self.protocol.createNode(JID('pubsub.example.org'), 'test')
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'create', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_createNodeInstant(self):
"""
Test sending create request resulting in an instant node.
"""
def cb(nodeIdentifier):
self.assertEquals('test', nodeIdentifier)
d = self.protocol.createNode(JID('pubsub.example.org'))
d.addCallback(cb)
iq = self.stub.output[-1]
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'create', NS_PUBSUB))
child = children[0]
self.assertFalse(child.hasAttribute('node'))
response = toResponse(iq, 'result')
command = response.addElement((NS_PUBSUB, 'pubsub'))
create = command.addElement('create')
create['node'] = 'test'
self.stub.send(response)
return d
def test_createNodeRenamed(self):
"""
Test sending create request resulting in renamed node.
"""
def cb(nodeIdentifier):
self.assertEquals('test2', nodeIdentifier)
d = self.protocol.createNode(JID('pubsub.example.org'), 'test')
d.addCallback(cb)
iq = self.stub.output[-1]
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'create', NS_PUBSUB))
child = children[0]
self.assertEquals('test', child['node'])
response = toResponse(iq, 'result')
command = response.addElement((NS_PUBSUB, 'pubsub'))
create = command.addElement('create')
create['node'] = 'test2'
self.stub.send(response)
return d
def test_createNodeWithSender(self):
"""
Test sending create request from a specific JID.
"""
d = self.protocol.createNode(JID('pubsub.example.org'), 'test',
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_createNodeWithConfig(self):
"""
Test sending create request with configuration options
"""
options = {
'pubsub#title': 'Princely Musings (Atom)',
'pubsub#deliver_payloads': True,
'pubsub#persist_items': '1',
'pubsub#max_items': '10',
'pubsub#access_model': 'open',
'pubsub#type': 'http://www.w3.org/2005/Atom',
}
d = self.protocol.createNode(JID('pubsub.example.org'), 'test',
sender=JID('user@example.org'),
options=options)
iq = self.stub.output[-1]
# check if there is exactly one configure element
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'configure', NS_PUBSUB))
self.assertEqual(1, len(children))
# check that it has a configuration form
form = data_form.findForm(children[0], NS_PUBSUB_NODE_CONFIG)
self.assertEqual('submit', form.formType)
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_deleteNode(self):
"""
Test sending delete request.
"""
d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test')
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB_OWNER, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'delete', NS_PUBSUB_OWNER))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_deleteNodeWithSender(self):
"""
Test sending delete request.
"""
d = self.protocol.deleteNode(JID('pubsub.example.org'), 'test',
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_publish(self):
"""
Test sending publish request.
"""
item = pubsub.Item()
d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item])
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'publish', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
items = list(domish.generateElementsQNamed(child.children,
'item', NS_PUBSUB))
self.assertEquals(1, len(items))
self.assertIdentical(item, items[0])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_publishNoItems(self):
"""
Test sending publish request without items.
"""
d = self.protocol.publish(JID('pubsub.example.org'), 'test')
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'publish', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_publishWithSender(self):
"""
Test sending publish request from a specific JID.
"""
item = pubsub.Item()
d = self.protocol.publish(JID('pubsub.example.org'), 'test', [item],
JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_subscribe(self):
"""
Test sending subscription request.
"""
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'subscribe', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
self.assertEquals('user@example.org', child['jid'])
response = toResponse(iq, 'result')
pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = pubsub.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'subscribed'
self.stub.send(response)
return d
def test_subscribeReturnsSubscription(self):
"""
A successful subscription should return a Subscription instance.
"""
def cb(subscription):
self.assertEqual(JID('user@example.org'), subscription.subscriber)
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
d.addCallback(cb)
iq = self.stub.output[-1]
response = toResponse(iq, 'result')
pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = pubsub.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'subscribed'
self.stub.send(response)
return d
def test_subscribePending(self):
"""
Test sending subscription request that results in a pending
subscription.
"""
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
iq = self.stub.output[-1]
response = toResponse(iq, 'result')
command = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = command.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'pending'
self.stub.send(response)
self.assertFailure(d, pubsub.SubscriptionPending)
return d
def test_subscribeUnconfigured(self):
"""
Test sending subscription request that results in an unconfigured
subscription.
"""
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
iq = self.stub.output[-1]
response = toResponse(iq, 'result')
command = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = command.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'unconfigured'
self.stub.send(response)
self.assertFailure(d, pubsub.SubscriptionUnconfigured)
return d
def test_subscribeWithOptions(self):
options = {'pubsub#deliver': False}
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
options=options)
iq = self.stub.output[-1]
# Check options present
childNames = []
for element in iq.pubsub.elements():
if element.uri == NS_PUBSUB:
childNames.append(element.name)
self.assertEqual(['subscribe', 'options'], childNames)
form = data_form.findForm(iq.pubsub.options,
NS_PUBSUB_SUBSCRIBE_OPTIONS)
self.assertEqual('submit', form.formType)
form.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
self.assertEqual(options, form.getValues())
# Send response
response = toResponse(iq, 'result')
pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = pubsub.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'subscribed'
self.stub.send(response)
return d
def test_subscribeWithSender(self):
"""
Test sending subscription request from a specific JID.
"""
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
response = toResponse(iq, 'result')
pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = pubsub.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'subscribed'
self.stub.send(response)
return d
def test_subscribeReturningSubscriptionIdentifier(self):
"""
Test sending subscription request with subscription identifier.
"""
def cb(subscription):
self.assertEqual('1234', subscription.subscriptionIdentifier)
d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
d.addCallback(cb)
iq = self.stub.output[-1]
response = toResponse(iq, 'result')
pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
subscription = pubsub.addElement('subscription')
subscription['node'] = 'test'
subscription['jid'] = 'user@example.org'
subscription['subscription'] = 'subscribed'
subscription['subid'] = '1234'
self.stub.send(response)
return d
def test_unsubscribe(self):
"""
Test sending unsubscription request.
"""
d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'unsubscribe', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
self.assertEquals('user@example.org', child['jid'])
self.stub.send(toResponse(iq, 'result'))
return d
def test_unsubscribeWithSender(self):
"""
Test sending unsubscription request from a specific JID.
"""
d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
self.stub.send(toResponse(iq, 'result'))
return d
def test_unsubscribeWithSubscriptionIdentifier(self):
"""
Test sending unsubscription request with subscription identifier.
"""
d = self.protocol.unsubscribe(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
subscriptionIdentifier='1234')
iq = self.stub.output[-1]
child = iq.pubsub.unsubscribe
self.assertEquals('1234', child['subid'])
self.stub.send(toResponse(iq, 'result'))
return d
def test_items(self):
"""
Test sending items request.
"""
def cb(items):
self.assertEquals([], items)
d = self.protocol.items(JID('pubsub.example.org'), 'test')
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('get', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'items', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
response = toResponse(iq, 'result')
items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items')
items['node'] = 'test'
self.stub.send(response)
return d
def test_itemsMaxItems(self):
"""
Test sending items request, with limit on the number of items.
"""
def cb(items):
self.assertEquals(2, len(items))
self.assertEquals([item1, item2], items)
d = self.protocol.items(JID('pubsub.example.org'), 'test', maxItems=2)
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('get', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'items', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
self.assertEquals('2', child['max_items'])
response = toResponse(iq, 'result')
items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items')
items['node'] = 'test'
item1 = items.addElement('item')
item1['id'] = 'item1'
item2 = items.addElement('item')
item2['id'] = 'item2'
self.stub.send(response)
return d
def test_itemsWithSubscriptionIdentifier(self):
"""
Test sending items request with a subscription identifier.
"""
d = self.protocol.items(JID('pubsub.example.org'), 'test',
subscriptionIdentifier='1234')
iq = self.stub.output[-1]
child = iq.pubsub.items
self.assertEquals('1234', child['subid'])
response = toResponse(iq, 'result')
items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items')
items['node'] = 'test'
self.stub.send(response)
return d
def test_itemsWithSender(self):
"""
Test sending items request from a specific JID.
"""
d = self.protocol.items(JID('pubsub.example.org'), 'test',
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])
response = toResponse(iq, 'result')
items = response.addElement((NS_PUBSUB, 'pubsub')).addElement('items')
items['node'] = 'test'
self.stub.send(response)
return d
def test_getOptions(self):
def cb(form):
self.assertEqual('form', form.formType)
self.assertEqual(NS_PUBSUB_SUBSCRIBE_OPTIONS, form.formNamespace)
field = form.fields['pubsub#deliver']
self.assertEqual('boolean', field.fieldType)
self.assertIdentical(True, field.value)
self.assertEqual('Enable delivery?', field.label)
d = self.protocol.getOptions(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
sender=JID('user@example.org'))
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEqual('pubsub.example.org', iq.getAttribute('to'))
self.assertEqual('get', iq.getAttribute('type'))
self.assertEqual('pubsub', iq.pubsub.name)
self.assertEqual(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'options', NS_PUBSUB))
self.assertEqual(1, len(children))
child = children[0]
self.assertEqual('test', child['node'])
self.assertEqual(0, len(child.children))
# Send response
form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
form.addField(data_form.Field('boolean', var='pubsub#deliver',
label='Enable delivery?',
value=True))
response = toResponse(iq, 'result')
response.addElement((NS_PUBSUB, 'pubsub'))
response.pubsub.addElement('options')
response.pubsub.options.addChild(form.toElement())
self.stub.send(response)
return d
def test_getOptionsWithSubscriptionIdentifier(self):
"""
Getting options with a subid should have the subid in the request.
"""
d = self.protocol.getOptions(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
sender=JID('user@example.org'),
subscriptionIdentifier='1234')
iq = self.stub.output[-1]
child = iq.pubsub.options
self.assertEqual('1234', child['subid'])
# Send response
form = data_form.Form('form', formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
form.addField(data_form.Field('boolean', var='pubsub#deliver',
label='Enable delivery?',
value=True))
response = toResponse(iq, 'result')
response.addElement((NS_PUBSUB, 'pubsub'))
response.pubsub.addElement('options')
response.pubsub.options.addChild(form.toElement())
self.stub.send(response)
return d
def test_setOptions(self):
"""
setOptions should send out a options-set request.
"""
options = {'pubsub#deliver': False}
d = self.protocol.setOptions(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
options,
sender=JID('user@example.org'))
iq = self.stub.output[-1]
self.assertEqual('pubsub.example.org', iq.getAttribute('to'))
self.assertEqual('set', iq.getAttribute('type'))
self.assertEqual('pubsub', iq.pubsub.name)
self.assertEqual(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'options', NS_PUBSUB))
self.assertEqual(1, len(children))
child = children[0]
self.assertEqual('test', child['node'])
form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS)
self.assertEqual('submit', form.formType)
form.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
self.assertEqual(options, form.getValues())
response = toResponse(iq, 'result')
self.stub.send(response)
return d
def test_setOptionsWithSubscriptionIdentifier(self):
"""
setOptions should send out a options-set request with subid.
"""
options = {'pubsub#deliver': False}
d = self.protocol.setOptions(JID('pubsub.example.org'), 'test',
JID('user@example.org'),
options,
subscriptionIdentifier='1234',
sender=JID('user@example.org'))
iq = self.stub.output[-1]
child = iq.pubsub.options
self.assertEqual('1234', child['subid'])
form = data_form.findForm(child, NS_PUBSUB_SUBSCRIBE_OPTIONS)
self.assertEqual('submit', form.formType)
form.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
self.assertEqual(options, form.getValues())
response = toResponse(iq, 'result')
self.stub.send(response)
return d
class PubSubRequestTest(unittest.TestCase):
def test_fromElementUnknown(self):
"""
An unknown verb raises NotImplementedError.
"""
xml = """
"""
self.assertRaises(NotImplementedError,
pubsub.PubSubRequest.fromElement, parseXml(xml))
def test_fromElementKnownBadCombination(self):
"""
Multiple verbs in an unknown configuration raises NotImplementedError.
"""
xml = """
"""
self.assertRaises(NotImplementedError,
pubsub.PubSubRequest.fromElement, parseXml(xml))
def test_fromElementPublish(self):
"""
Test parsing a publish request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('publish', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual([], request.items)
def test_fromElementPublishItems(self):
"""
Test parsing a publish request with items.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual(2, len(request.items))
self.assertEqual(u'item1', request.items[0]["id"])
self.assertEqual(u'item2', request.items[1]["id"])
def test_fromElementPublishItemsOptions(self):
"""
Test parsing a publish request with items and options.
Note that publishing options are not supported, but passing them
shouldn't affect processing of the publish request itself.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual(2, len(request.items))
self.assertEqual(u'item1', request.items[0]["id"])
self.assertEqual(u'item2', request.items[1]["id"])
def test_fromElementPublishNoNode(self):
"""
A publish request to the root node should raise an exception.
"""
xml = """
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
self.assertEqual('nodeid-required', err.appCondition.name)
def test_fromElementSubscribe(self):
"""
Test parsing a subscription request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('subscribe', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual(JID('user@example.org/Home'), request.subscriber)
def test_fromElementSubscribeEmptyNode(self):
"""
Test parsing a subscription request to the root node.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('', request.nodeIdentifier)
def test_fromElementSubscribeNoJID(self):
"""
Subscribe requests without a JID should raise a bad-request exception.
"""
xml = """
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
self.assertEqual('jid-required', err.appCondition.name)
def test_fromElementSubscribeWithOptions(self):
"""
Test parsing a subscription request.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('subscribe', request.verb)
request.options.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
self.assertEqual({'pubsub#deliver': True}, request.options.getValues())
def test_fromElementSubscribeWithOptionsBadFormType(self):
"""
The options form should have the right type.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual("Unexpected form type 'result'", err.text)
self.assertEqual(None, err.appCondition)
def test_fromElementSubscribeWithOptionsEmpty(self):
"""
When no (suitable) form is found, the options are empty.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('subscribe', request.verb)
self.assertEqual({}, request.options.getValues())
def test_fromElementUnsubscribe(self):
"""
Test parsing an unsubscription request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('unsubscribe', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual(JID('user@example.org/Home'), request.subscriber)
def test_fromElementUnsubscribeWithSubscriptionIdentifier(self):
"""
Test parsing an unsubscription request with subscription identifier.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('1234', request.subscriptionIdentifier)
def test_fromElementUnsubscribeNoJID(self):
"""
Unsubscribe requests without a JID should raise a bad-request exception.
"""
xml = """
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
self.assertEqual('jid-required', err.appCondition.name)
def test_fromElementOptionsGet(self):
"""
Test parsing a request for getting subscription options.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('optionsGet', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual(JID('user@example.org/Home'), request.subscriber)
def test_fromElementOptionsGetWithSubscriptionIdentifier(self):
"""
Test parsing a request for getting subscription options with subid.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('1234', request.subscriptionIdentifier)
def test_fromElementOptionsSet(self):
"""
Test parsing a request for setting subscription options.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('optionsSet', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual(JID('user@example.org/Home'), request.subscriber)
self.assertEqual({'pubsub#deliver': '1'}, request.options.getValues())
def test_fromElementOptionsSetWithSubscriptionIdentifier(self):
"""
Test parsing a request for setting subscription options with subid.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('1234', request.subscriptionIdentifier)
def test_fromElementOptionsSetCancel(self):
"""
Test parsing a request for cancelling setting subscription options.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('cancel', request.options.formType)
def test_fromElementOptionsSetBadFormType(self):
"""
On a options set request unknown fields should be ignored.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual("Unexpected form type 'result'", err.text)
self.assertEqual(None, err.appCondition)
def test_fromElementOptionsSetNoForm(self):
"""
On a options set request a form is required.
"""
xml = """
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual(None, err.appCondition)
def test_fromElementSubscriptions(self):
"""
Test parsing a request for all subscriptions.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('subscriptions', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
def test_fromElementAffiliations(self):
"""
Test parsing a request for all affiliations.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('affiliations', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
def test_fromElementCreate(self):
"""
Test parsing a request to create a node.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('create', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('mynode', request.nodeIdentifier)
self.assertIdentical(None, request.options)
def test_fromElementCreateInstant(self):
"""
Test parsing a request to create an instant node.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertIdentical(None, request.nodeIdentifier)
def test_fromElementCreateConfigureEmpty(self):
"""
Test parsing a request to create a node with an empty configuration.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual({}, request.options.getValues())
self.assertEqual(u'mynode', request.nodeIdentifier)
def test_fromElementCreateConfigureEmptyWrongOrder(self):
"""
Test parsing a request to create a node and configure, wrong order.
The C{configure} element should come after the C{create} request,
but we should accept both orders.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual({}, request.options.getValues())
self.assertEqual(u'mynode', request.nodeIdentifier)
def test_fromElementCreateConfigure(self):
"""
Test parsing a request to create a node.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
open
0
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
values = request.options
self.assertIn('pubsub#access_model', values)
self.assertEqual(u'open', values['pubsub#access_model'])
self.assertIn('pubsub#persist_items', values)
self.assertEqual(u'0', values['pubsub#persist_items'])
def test_fromElementCreateConfigureBadFormType(self):
"""
The form of a node creation request should have the right type.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
open
0
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual("Unexpected form type 'result'", err.text)
self.assertEqual(None, err.appCondition)
def test_fromElementDefault(self):
"""
Parsing default node configuration request sets required attributes.
Besides C{verb}, C{sender} and C{recipient}, we expect C{nodeType}
to be set. If not passed it receives the default C{u'leaf'}.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEquals(u'default', request.verb)
self.assertEquals(JID('user@example.org'), request.sender)
self.assertEquals(JID('pubsub.example.org'), request.recipient)
self.assertEquals(u'leaf', request.nodeType)
def test_fromElementDefaultCollection(self):
"""
Parsing default request for collection sets nodeType to collection.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
collection
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEquals('collection', request.nodeType)
def test_fromElementConfigureGet(self):
"""
Test parsing a node configuration get request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('configureGet', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
def test_fromElementConfigureSet(self):
"""
On a node configuration set request the Data Form is parsed.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('configureSet', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual({'pubsub#deliver_payloads': '0',
'pubsub#persist_items': '1'},
request.options.getValues())
def test_fromElementConfigureSetCancel(self):
"""
The node configuration is cancelled, so no options.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('cancel', request.options.formType)
def test_fromElementConfigureSetBadFormType(self):
"""
The form of a node configuraton set request should have the right type.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual("Unexpected form type 'result'", err.text)
self.assertEqual(None, err.appCondition)
def test_fromElementConfigureSetNoForm(self):
"""
On a node configuration set request a form is required.
"""
xml = """
"""
err = self.assertRaises(error.StanzaError,
pubsub.PubSubRequest.fromElement,
parseXml(xml))
self.assertEqual('bad-request', err.condition)
self.assertEqual(None, err.appCondition)
def test_fromElementItems(self):
"""
Test parsing an items request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('items', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertIdentical(None, request.maxItems)
self.assertIdentical(None, request.subscriptionIdentifier)
self.assertEqual([], request.itemIdentifiers)
def test_fromElementItemsSubscriptionIdentifier(self):
"""
Test parsing an items request with subscription identifier.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('1234', request.subscriptionIdentifier)
def test_fromElementRetract(self):
"""
Test parsing a retract request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('retract', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
self.assertEqual(['item1', 'item2'], request.itemIdentifiers)
def test_fromElementPurge(self):
"""
Test parsing a purge request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('purge', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
def test_fromElementDelete(self):
"""
Test parsing a delete request.
"""
xml = """
"""
request = pubsub.PubSubRequest.fromElement(parseXml(xml))
self.assertEqual('delete', request.verb)
self.assertEqual(JID('user@example.org'), request.sender)
self.assertEqual(JID('pubsub.example.org'), request.recipient)
self.assertEqual('test', request.nodeIdentifier)
class PubSubServiceTest(unittest.TestCase, TestableRequestHandlerMixin):
"""
Tests for L{pubsub.PubSubService}.
"""
def setUp(self):
self.stub = XmlStreamStub()
self.resource = pubsub.PubSubResource()
self.service = pubsub.PubSubService(self.resource)
self.service.send = self.stub.xmlstream.send
def test_interface(self):
"""
Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}?
"""
verify.verifyObject(iwokkel.IPubSubService, self.service)
def test_interfaceIDisco(self):
"""
Do instances of L{pubsub.PubSubService} provide L{iwokkel.IDisco}?
"""
verify.verifyObject(iwokkel.IDisco, self.service)
def test_connectionMade(self):
"""
Verify setup of observers in L{pubsub.connectionMade}.
"""
requests = []
def handleRequest(iq):
requests.append(iq)
self.service.xmlstream = self.stub.xmlstream
self.service.handleRequest = handleRequest
self.service.connectionMade()
for namespace in (NS_PUBSUB, NS_PUBSUB_OWNER):
for stanzaType in ('get', 'set'):
iq = domish.Element((None, 'iq'))
iq['type'] = stanzaType
iq.addElement((namespace, 'pubsub'))
self.stub.xmlstream.dispatch(iq)
self.assertEqual(4, len(requests))
def test_getDiscoInfo(self):
"""
Test getDiscoInfo calls getNodeInfo and returns some minimal info.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn(('pubsub', 'service'), discoInfo.identities)
self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features)
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), '')
d.addCallback(cb)
return d
def test_getDiscoInfoNodeType(self):
"""
Test getDiscoInfo with node type.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn(('pubsub', 'collection'), discoInfo.identities)
def getInfo(requestor, target, nodeIdentifier):
return defer.succeed({'type': 'collection',
'meta-data': {}})
self.resource.getInfo = getInfo
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), '')
d.addCallback(cb)
return d
def test_getDiscoInfoMetaData(self):
"""
Test getDiscoInfo with returned meta data.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn(('pubsub', 'leaf'), discoInfo.identities)
self.assertIn(NS_PUBSUB_META_DATA, discoInfo.extensions)
form = discoInfo.extensions[NS_PUBSUB_META_DATA]
self.assertIn('pubsub#node_type', form.fields)
def getInfo(requestor, target, nodeIdentifier):
metaData = [{'var': 'pubsub#persist_items',
'label': 'Persist items to storage',
'value': True}]
return defer.succeed({'type': 'leaf', 'meta-data': metaData})
self.resource.getInfo = getInfo
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), '')
d.addCallback(cb)
return d
def test_getDiscoInfoResourceFeatures(self):
"""
Test getDiscoInfo with the resource features.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn('http://jabber.org/protocol/pubsub#publish',
discoInfo.features)
self.resource.features = ['publish']
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), '')
d.addCallback(cb)
return d
def test_getDiscoInfoBadResponse(self):
"""
If getInfo returns invalid response, it should be logged, then ignored.
"""
def cb(info):
self.assertEquals([], info)
self.assertEqual(1, len(self.flushLoggedErrors(TypeError)))
def getInfo(requestor, target, nodeIdentifier):
return defer.succeed('bad response')
self.resource.getInfo = getInfo
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), 'test')
d.addCallback(cb)
return d
def test_getDiscoInfoException(self):
"""
If getInfo returns invalid response, it should be logged, then ignored.
"""
def cb(info):
self.assertEquals([], info)
self.assertEqual(1, len(self.flushLoggedErrors(NotImplementedError)))
def getInfo(requestor, target, nodeIdentifier):
return defer.fail(NotImplementedError())
self.resource.getInfo = getInfo
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), 'test')
d.addCallback(cb)
return d
def test_getDiscoItemsRoot(self):
"""
Test getDiscoItems on the root node.
"""
def getNodes(requestor, service, nodeIdentifier):
return defer.succeed(['node1', 'node2'])
def cb(items):
self.assertEqual(2, len(items))
item1, item2 = items
self.assertEqual(JID('pubsub.example.org'), item1.entity)
self.assertEqual('node1', item1.nodeIdentifier)
self.assertEqual(JID('pubsub.example.org'), item2.entity)
self.assertEqual('node2', item2.nodeIdentifier)
self.resource.getNodes = getNodes
d = self.service.getDiscoItems(JID('user@example.org/home'),
JID('pubsub.example.org'),
'')
d.addCallback(cb)
return d
def test_getDiscoItemsRootHideNodes(self):
"""
Test getDiscoItems on the root node.
"""
def getNodes(requestor, service, nodeIdentifier):
raise Exception("Unexpected call to getNodes")
def cb(items):
self.assertEqual([], items)
self.service.hideNodes = True
self.resource.getNodes = getNodes
d = self.service.getDiscoItems(JID('user@example.org/home'),
JID('pubsub.example.org'),
'')
d.addCallback(cb)
return d
def test_getDiscoItemsNonRoot(self):
"""
Test getDiscoItems on a non-root node.
"""
def getNodes(requestor, service, nodeIdentifier):
return defer.succeed(['node1', 'node2'])
def cb(items):
self.assertEqual(2, len(items))
self.resource.getNodes = getNodes
d = self.service.getDiscoItems(JID('user@example.org/home'),
JID('pubsub.example.org'),
'test')
d.addCallback(cb)
return d
def test_on_publish(self):
"""
A publish request should result in L{PubSubService.publish} being
called.
"""
xml = """
"""
def publish(request):
return defer.succeed(None)
self.resource.publish = publish
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_subscribe(self):
"""
A successful subscription should return the current subscription.
"""
xml = """
"""
def subscribe(request):
return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
request.subscriber,
'subscribed'))
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB, element.uri)
subscription = element.subscription
self.assertEqual(NS_PUBSUB, subscription.uri)
self.assertEqual('test', subscription['node'])
self.assertEqual('user@example.org/Home', subscription['jid'])
self.assertEqual('subscribed', subscription['subscription'])
self.resource.subscribe = subscribe
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_subscribeEmptyNode(self):
"""
A successful subscription on root node should return no node attribute.
"""
xml = """
"""
def subscribe(request):
return defer.succeed(pubsub.Subscription(request.nodeIdentifier,
request.subscriber,
'subscribed'))
def cb(element):
self.assertFalse(element.subscription.hasAttribute('node'))
self.resource.subscribe = subscribe
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_subscribeSubscriptionIdentifier(self):
"""
If a subscription returns a subid, this should be available.
"""
xml = """
"""
def subscribe(request):
subscription = pubsub.Subscription(request.nodeIdentifier,
request.subscriber,
'subscribed',
subscriptionIdentifier='1234')
return defer.succeed(subscription)
def cb(element):
self.assertEqual('1234', element.subscription.getAttribute('subid'))
self.resource.subscribe = subscribe
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_unsubscribe(self):
"""
A successful unsubscription should return an empty response.
"""
xml = """
"""
def unsubscribe(request):
return defer.succeed(None)
def cb(element):
self.assertIdentical(None, element)
self.resource.unsubscribe = unsubscribe
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_unsubscribeSubscriptionIdentifier(self):
"""
A successful unsubscription with subid should return an empty response.
"""
xml = """
"""
def unsubscribe(request):
self.assertEqual('1234', request.subscriptionIdentifier)
return defer.succeed(None)
def cb(element):
self.assertIdentical(None, element)
self.resource.unsubscribe = unsubscribe
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_optionsGet(self):
"""
Getting subscription options is not supported.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_optionsSet(self):
"""
Setting subscription options is not supported.
"""
xml = """
http://jabber.org/protocol/pubsub#subscribe_options
1
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_subscriptions(self):
"""
A subscriptions request should result in
L{PubSubService.subscriptions} being called and the result prepared
for the response.
"""
xml = """
"""
def subscriptions(request):
subscription = pubsub.Subscription('test', JID('user@example.org'),
'subscribed')
return defer.succeed([subscription])
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB, element.uri)
self.assertEqual(NS_PUBSUB, element.subscriptions.uri)
children = list(element.subscriptions.elements())
self.assertEqual(1, len(children))
subscription = children[0]
self.assertEqual('subscription', subscription.name)
self.assertEqual(NS_PUBSUB, subscription.uri, NS_PUBSUB)
self.assertEqual('user@example.org', subscription['jid'])
self.assertEqual('test', subscription['node'])
self.assertEqual('subscribed', subscription['subscription'])
self.resource.subscriptions = subscriptions
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_subscriptionsWithSubscriptionIdentifier(self):
"""
A subscriptions request response should include subids, if set.
"""
xml = """
"""
def subscriptions(request):
subscription = pubsub.Subscription('test', JID('user@example.org'),
'subscribed',
subscriptionIdentifier='1234')
return defer.succeed([subscription])
def cb(element):
subscription = element.subscriptions.subscription
self.assertEqual('1234', subscription['subid'])
self.resource.subscriptions = subscriptions
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_affiliations(self):
"""
A subscriptions request should result in
L{PubSubService.affiliations} being called and the result prepared
for the response.
"""
xml = """
"""
def affiliations(request):
affiliation = ('test', 'owner')
return defer.succeed([affiliation])
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB, element.uri)
self.assertEqual(NS_PUBSUB, element.affiliations.uri)
children = list(element.affiliations.elements())
self.assertEqual(1, len(children))
affiliation = children[0]
self.assertEqual('affiliation', affiliation.name)
self.assertEqual(NS_PUBSUB, affiliation.uri)
self.assertEqual('test', affiliation['node'])
self.assertEqual('owner', affiliation['affiliation'])
self.resource.affiliations = affiliations
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_create(self):
"""
Replies to create node requests don't return the created node.
"""
xml = """
"""
def create(request):
return defer.succeed(request.nodeIdentifier)
def cb(element):
self.assertIdentical(None, element)
self.resource.create = create
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_createChanged(self):
"""
Replies to create node requests return the created node if changed.
"""
xml = """
"""
def create(request):
return defer.succeed(u'myrenamednode')
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB, element.uri)
self.assertEqual(NS_PUBSUB, element.create.uri)
self.assertEqual(u'myrenamednode',
element.create.getAttribute('node'))
self.resource.create = create
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_createInstant(self):
"""
Replies to create instant node requests return the created node.
"""
xml = """
"""
def create(request):
return defer.succeed(u'random')
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB, element.uri)
self.assertEqual(NS_PUBSUB, element.create.uri)
self.assertEqual(u'random', element.create.getAttribute('node'))
self.resource.create = create
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_createWithConfig(self):
"""
On a node create with configuration request the Data Form is parsed and
L{PubSubResource.create} is called with the passed options.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def getConfigurationOptions():
return {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"}
}
def create(request):
self.assertEqual({'pubsub#deliver_payloads': False,
'pubsub#persist_items': True},
request.options.getValues())
return defer.succeed(None)
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.create = create
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_default(self):
"""
A default request returns default options filtered by available fields.
"""
xml = """
"""
fieldDefs = {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"}
}
def getConfigurationOptions():
return fieldDefs
def default(request):
return defer.succeed({'pubsub#persist_items': 'false',
'x-myfield': '1'})
def cb(element):
self.assertEquals('pubsub', element.name)
self.assertEquals(NS_PUBSUB_OWNER, element.uri)
self.assertEquals(NS_PUBSUB_OWNER, element.default.uri)
form = data_form.Form.fromElement(element.default.x)
self.assertEquals(NS_PUBSUB_NODE_CONFIG, form.formNamespace)
form.typeCheck(fieldDefs)
self.assertIn('pubsub#persist_items', form.fields)
self.assertFalse(form.fields['pubsub#persist_items'].value)
self.assertNotIn('x-myfield', form.fields)
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.default = default
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_defaultUnknownNodeType(self):
"""
Unknown node types yield non-acceptable.
Both C{getConfigurationOptions} and C{default} must not be called.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
unknown
"""
def getConfigurationOptions():
self.fail("Unexpected call to getConfigurationOptions")
def default(request):
self.fail("Unexpected call to default")
def cb(result):
self.assertEquals('not-acceptable', result.condition)
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.default = default
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_configureGet(self):
"""
On a node configuration get
requestL{PubSubResource.configureGet} is called and results in a
data form with the configuration.
"""
xml = """
"""
def getConfigurationOptions():
return {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"},
"pubsub#owner":
{"type": "jid-single",
"label": "Owner of the node"}
}
def configureGet(request):
return defer.succeed({'pubsub#deliver_payloads': '0',
'pubsub#persist_items': '1',
'pubsub#owner': JID('user@example.org'),
'x-myfield': 'a'})
def cb(element):
self.assertEqual('pubsub', element.name)
self.assertEqual(NS_PUBSUB_OWNER, element.uri)
self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri)
form = data_form.Form.fromElement(element.configure.x)
self.assertEqual(NS_PUBSUB_NODE_CONFIG, form.formNamespace)
fields = form.fields
self.assertIn('pubsub#deliver_payloads', fields)
field = fields['pubsub#deliver_payloads']
self.assertEqual('boolean', field.fieldType)
field.typeCheck()
self.assertEqual(False, field.value)
self.assertIn('pubsub#persist_items', fields)
field = fields['pubsub#persist_items']
self.assertEqual('boolean', field.fieldType)
field.typeCheck()
self.assertEqual(True, field.value)
self.assertIn('pubsub#owner', fields)
field = fields['pubsub#owner']
self.assertEqual('jid-single', field.fieldType)
field.typeCheck()
self.assertEqual(JID('user@example.org'), field.value)
self.assertNotIn('x-myfield', fields)
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.configureGet = configureGet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_configureSet(self):
"""
On a node configuration set request the Data Form is parsed and
L{PubSubResource.configureSet} is called with the passed options.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def getConfigurationOptions():
return {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"}
}
def configureSet(request):
self.assertEqual({'pubsub#deliver_payloads': False,
'pubsub#persist_items': True},
request.options.getValues())
return defer.succeed(None)
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.configureSet = configureSet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_configureSetCancel(self):
"""
The node configuration is cancelled,
L{PubSubResource.configureSet} not called.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
"""
def configureSet(request):
self.fail("Unexpected call to setConfiguration")
self.resource.configureSet = configureSet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_configureSetIgnoreUnknown(self):
"""
On a node configuration set request unknown fields should be ignored.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def getConfigurationOptions():
return {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"}
}
def configureSet(request):
self.assertEquals(['pubsub#deliver_payloads'],
request.options.keys())
self.resource.getConfigurationOptions = getConfigurationOptions
self.resource.configureSet = configureSet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_configureSetBadFormType(self):
"""
On a node configuration set request unknown fields should be ignored.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def cb(result):
self.assertEquals('bad-request', result.condition)
self.assertEqual("Unexpected form type 'result'", result.text)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_items(self):
"""
On a items request, return all items for the given node.
"""
xml = """
"""
def items(request):
return defer.succeed([pubsub.Item('current')])
def cb(element):
self.assertEqual(NS_PUBSUB, element.uri)
self.assertEqual(NS_PUBSUB, element.items.uri)
self.assertEqual(1, len(element.items.children))
item = element.items.children[-1]
self.assertTrue(domish.IElement.providedBy(item))
self.assertEqual('item', item.name)
self.assertEqual(NS_PUBSUB, item.uri)
self.assertEqual('current', item['id'])
self.resource.items = items
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_retract(self):
"""
A retract request should result in L{PubSubResource.retract}
being called.
"""
xml = """
"""
def retract(request):
return defer.succeed(None)
self.resource.retract = retract
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_purge(self):
"""
A purge request should result in L{PubSubResource.purge} being
called.
"""
xml = """
"""
def purge(request):
return defer.succeed(None)
self.resource.purge = purge
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_on_delete(self):
"""
A delete request should result in L{PubSubResource.delete} being
called.
"""
xml = """
"""
def delete(request):
return defer.succeed(None)
self.resource.delete = delete
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
return self.handleRequest(xml)
def test_notifyPublish(self):
"""
Publish notifications are sent to the subscribers.
"""
subscriber = JID('user@example.org')
subscriptions = [pubsub.Subscription('test', subscriber, 'subscribed')]
items = [pubsub.Item('current')]
notifications = [(subscriber, subscriptions, items)]
self.service.notifyPublish(JID('pubsub.example.org'), 'test',
notifications)
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertIdentical(None, message.uri)
self.assertEquals('user@example.org', message['to'])
self.assertEquals('pubsub.example.org', message['from'])
self.assertTrue(message.event)
self.assertEquals(NS_PUBSUB_EVENT, message.event.uri)
self.assertTrue(message.event.items)
self.assertEquals(NS_PUBSUB_EVENT, message.event.items.uri)
self.assertTrue(message.event.items.hasAttribute('node'))
self.assertEquals('test', message.event.items['node'])
itemElements = list(domish.generateElementsQNamed(
message.event.items.children, 'item', NS_PUBSUB_EVENT))
self.assertEquals(1, len(itemElements))
self.assertEquals('current', itemElements[0].getAttribute('id'))
def test_notifyPublishCollection(self):
"""
Publish notifications are sent to the subscribers of collections.
The node the item was published to is on the C{items} element, while
the subscribed-to node is in the C{'Collections'} SHIM header.
"""
subscriber = JID('user@example.org')
subscriptions = [pubsub.Subscription('', subscriber, 'subscribed')]
items = [pubsub.Item('current')]
notifications = [(subscriber, subscriptions, items)]
self.service.notifyPublish(JID('pubsub.example.org'), 'test',
notifications)
message = self.stub.output[-1]
self.assertTrue(message.event.items.hasAttribute('node'))
self.assertEquals('test', message.event.items['node'])
headers = shim.extractHeaders(message)
self.assertIn('Collection', headers)
self.assertIn('', headers['Collection'])
def test_notifyDelete(self):
"""
Subscribers should be sent a delete notification.
"""
subscriptions = [JID('user@example.org')]
self.service.notifyDelete(JID('pubsub.example.org'), 'test',
subscriptions)
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertIdentical(None, message.uri)
self.assertEquals('user@example.org', message['to'])
self.assertEquals('pubsub.example.org', message['from'])
self.assertTrue(message.event)
self.assertEqual(NS_PUBSUB_EVENT, message.event.uri)
self.assertTrue(message.event.delete)
self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri)
self.assertTrue(message.event.delete.hasAttribute('node'))
self.assertEqual('test', message.event.delete['node'])
def test_notifyDeleteRedirect(self):
"""
Subscribers should be sent a delete notification with redirect.
"""
redirectURI = 'xmpp:pubsub.example.org?;node=test2'
subscriptions = [JID('user@example.org')]
self.service.notifyDelete(JID('pubsub.example.org'), 'test',
subscriptions, redirectURI)
message = self.stub.output[-1]
self.assertEquals('message', message.name)
self.assertIdentical(None, message.uri)
self.assertEquals('user@example.org', message['to'])
self.assertEquals('pubsub.example.org', message['from'])
self.assertTrue(message.event)
self.assertEqual(NS_PUBSUB_EVENT, message.event.uri)
self.assertTrue(message.event.delete)
self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.uri)
self.assertTrue(message.event.delete.hasAttribute('node'))
self.assertEqual('test', message.event.delete['node'])
self.assertTrue(message.event.delete.redirect)
self.assertEqual(NS_PUBSUB_EVENT, message.event.delete.redirect.uri)
self.assertTrue(message.event.delete.redirect.hasAttribute('uri'))
self.assertEqual(redirectURI, message.event.delete.redirect['uri'])
def test_on_subscriptionsGet(self):
"""
Getting subscription options is not supported.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('manage-subscriptions',
result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_subscriptionsSet(self):
"""
Setting subscription options is not supported.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('manage-subscriptions',
result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_affiliationsGet(self):
"""
Getting node affiliations should have.
"""
xml = """
"""
def affiliationsGet(request):
self.assertEquals('test', request.nodeIdentifier)
return defer.succeed({JID('user@example.org'): 'owner'})
def cb(element):
self.assertEquals(u'pubsub', element.name)
self.assertEquals(NS_PUBSUB_OWNER, element.uri)
self.assertEquals(NS_PUBSUB_OWNER, element.affiliations.uri)
self.assertEquals(u'test', element.affiliations[u'node'])
children = list(element.affiliations.elements())
self.assertEquals(1, len(children))
affiliation = children[0]
self.assertEquals(u'affiliation', affiliation.name)
self.assertEquals(NS_PUBSUB_OWNER, affiliation.uri)
self.assertEquals(u'user@example.org', affiliation[u'jid'])
self.assertEquals(u'owner', affiliation[u'affiliation'])
self.resource.affiliationsGet = affiliationsGet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_affiliationsGetEmptyNode(self):
"""
Getting node affiliations without node should assume empty node.
"""
xml = """
"""
def affiliationsGet(request):
self.assertIdentical('', request.nodeIdentifier)
return defer.succeed({})
def cb(element):
self.assertFalse(element.affiliations.hasAttribute(u'node'))
self.resource.affiliationsGet = affiliationsGet
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
d = self.handleRequest(xml)
d.addCallback(cb)
return d
def test_on_affiliationsSet(self):
"""
Setting node affiliations has the affiliations to be modified.
"""
xml = """
"""
def affiliationsSet(request):
self.assertEquals(u'test', request.nodeIdentifier)
otherJID = JID(u'other@example.org')
self.assertIn(otherJID, request.affiliations)
self.assertEquals(u'publisher', request.affiliations[otherJID])
self.resource.affiliationsSet = affiliationsSet
return self.handleRequest(xml)
def test_on_affiliationsSetBareJID(self):
"""
Affiliations are always on the bare JID.
"""
xml = """
"""
def affiliationsSet(request):
otherJID = JID(u'other@example.org')
self.assertIn(otherJID, request.affiliations)
self.resource.affiliationsSet = affiliationsSet
return self.handleRequest(xml)
def test_on_affiliationsSetMultipleForSameEntity(self):
"""
Setting node affiliations can only have one item per entity.
"""
xml = """
"""
def cb(result):
self.assertEquals('bad-request', result.condition)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_affiliationsSetMissingJID(self):
"""
Setting node affiliations must include a JID per affiliation.
"""
xml = """
"""
def cb(result):
self.assertEquals('bad-request', result.condition)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_on_affiliationsSetMissingAffiliation(self):
"""
Setting node affiliations must include an affiliation.
"""
xml = """
"""
def cb(result):
self.assertEquals('bad-request', result.condition)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
class PubSubServiceWithoutResourceTest(unittest.TestCase, TestableRequestHandlerMixin):
def setUp(self):
self.stub = XmlStreamStub()
self.service = pubsub.PubSubService()
self.service.send = self.stub.xmlstream.send
def test_getDiscoInfo(self):
"""
Test getDiscoInfo calls getNodeInfo and returns some minimal info.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn(('pubsub', 'service'), discoInfo.identities)
self.assertIn(disco.NS_DISCO_ITEMS, discoInfo.features)
d = self.service.getDiscoInfo(JID('user@example.org/home'),
JID('pubsub.example.org'), '')
d.addCallback(cb)
return d
def test_publish(self):
"""
Non-overridden L{PubSubService.publish} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('publish', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_subscribe(self):
"""
Non-overridden L{PubSubService.subscribe} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('subscribe', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_unsubscribe(self):
"""
Non-overridden L{PubSubService.unsubscribe} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('subscribe', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_subscriptions(self):
"""
Non-overridden L{PubSubService.subscriptions} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-subscriptions',
result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_affiliations(self):
"""
Non-overridden L{PubSubService.affiliations} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-affiliations',
result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_create(self):
"""
Non-overridden L{PubSubService.create} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('create-nodes', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_getDefaultConfiguration(self):
"""
Non-overridden L{PubSubService.getDefaultConfiguration} yields
unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-default', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_getConfiguration(self):
"""
Non-overridden L{PubSubService.getConfiguration} yields unsupported
error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('config-node', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_setConfiguration(self):
"""
Non-overridden L{PubSubService.setConfiguration} yields unsupported
error.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('config-node', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_setConfigurationOptionsDict(self):
"""
Options should be passed as a dictionary, not a form.
"""
xml = """
http://jabber.org/protocol/pubsub#node_config
0
1
"""
def getConfigurationOptions():
return {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"}
}
def setConfiguration(requestor, service, nodeIdentifier, options):
self.assertIn('pubsub#deliver_payloads', options)
self.assertFalse(options['pubsub#deliver_payloads'])
self.assertIn('pubsub#persist_items', options)
self.assertTrue(options['pubsub#persist_items'])
self.service.getConfigurationOptions = getConfigurationOptions
self.service.setConfiguration = setConfiguration
return self.handleRequest(xml)
def test_items(self):
"""
Non-overridden L{PubSubService.items} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-items', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_retract(self):
"""
Non-overridden L{PubSubService.retract} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retract-items', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_purge(self):
"""
Non-overridden L{PubSubService.purge} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('purge-nodes', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_delete(self):
"""
Non-overridden L{PubSubService.delete} yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('delete-nodes', result.appCondition['feature'])
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_unknown(self):
"""
Unknown verb yields unsupported error.
"""
xml = """
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
d = self.handleRequest(xml)
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
class PubSubResourceTest(unittest.TestCase):
def setUp(self):
self.resource = pubsub.PubSubResource()
def test_interface(self):
"""
Do instances of L{pubsub.PubSubResource} provide L{iwokkel.IPubSubResource}?
"""
verify.verifyObject(iwokkel.IPubSubResource, self.resource)
def test_getNodes(self):
"""
Default getNodes returns an empty list.
"""
def cb(nodes):
self.assertEquals([], nodes)
d = self.resource.getNodes(JID('user@example.org/home'),
JID('pubsub.example.org'),
'')
d.addCallback(cb)
return d
def test_publish(self):
"""
Non-overridden L{PubSubResource.publish} yields unsupported
error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('publish', result.appCondition['feature'])
d = self.resource.publish(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_subscribe(self):
"""
Non-overridden subscriptions yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('subscribe', result.appCondition['feature'])
d = self.resource.subscribe(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_unsubscribe(self):
"""
Non-overridden unsubscribe yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('subscribe', result.appCondition['feature'])
d = self.resource.unsubscribe(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_subscriptions(self):
"""
Non-overridden subscriptions yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-subscriptions',
result.appCondition['feature'])
d = self.resource.subscriptions(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_affiliations(self):
"""
Non-overridden affiliations yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-affiliations',
result.appCondition['feature'])
d = self.resource.affiliations(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_create(self):
"""
Non-overridden create yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('create-nodes', result.appCondition['feature'])
d = self.resource.create(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_default(self):
"""
Non-overridden default yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-default',
result.appCondition['feature'])
d = self.resource.default(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_configureGet(self):
"""
Non-overridden configureGet yields unsupported
error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('config-node', result.appCondition['feature'])
d = self.resource.configureGet(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_configureSet(self):
"""
Non-overridden configureSet yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('config-node', result.appCondition['feature'])
d = self.resource.configureSet(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_items(self):
"""
Non-overridden items yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retrieve-items', result.appCondition['feature'])
d = self.resource.items(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_retract(self):
"""
Non-overridden retract yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('retract-items', result.appCondition['feature'])
d = self.resource.retract(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_purge(self):
"""
Non-overridden purge yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('purge-nodes', result.appCondition['feature'])
d = self.resource.purge(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_delete(self):
"""
Non-overridden delete yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('delete-nodes', result.appCondition['feature'])
d = self.resource.delete(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_affiliationsGet(self):
"""
Non-overridden owner affiliations get yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('modify-affiliations',
result.appCondition['feature'])
d = self.resource.affiliationsGet(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
def test_affiliationsSet(self):
"""
Non-overridden owner affiliations set yields unsupported error.
"""
def cb(result):
self.assertEquals('feature-not-implemented', result.condition)
self.assertEquals('unsupported', result.appCondition.name)
self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
self.assertEquals('modify-affiliations',
result.appCondition['feature'])
d = self.resource.affiliationsSet(pubsub.PubSubRequest())
self.assertFailure(d, error.StanzaError)
d.addCallback(cb)
return d
wokkel-0.7.1/wokkel/test/test_data_form.py 0000775 0001750 0001750 00000135712 11707215356 021450 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for {wokkel.data_form}.
"""
from zope.interface import verify
from zope.interface.common.mapping import IIterableMapping
from twisted.trial import unittest
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid
from wokkel import data_form
NS_X_DATA = 'jabber:x:data'
class OptionTest(unittest.TestCase):
"""
Tests for L{data_form.Option}.
"""
def test_toElement(self):
"""
An option is an option element with a value child with the option value.
"""
option = data_form.Option('value')
element = option.toElement()
self.assertEqual('option', element.name)
self.assertEqual(NS_X_DATA, element.uri)
self.assertEqual(NS_X_DATA, element.value.uri)
self.assertEqual('value', unicode(element.value))
self.assertFalse(element.hasAttribute('label'))
def test_toElementLabel(self):
"""
A label is rendered as an attribute on the option element.
"""
option = data_form.Option('value', 'label')
element = option.toElement()
self.assertEqual('option', element.name)
self.assertEqual(NS_X_DATA, element.uri)
self.assertEqual(NS_X_DATA, element.value.uri)
self.assertEqual('value', unicode(element.value))
self.assertEqual('label', element['label'])
def test_fromElement(self):
"""
An option has a child element with the option value.
"""
element = domish.Element((NS_X_DATA, 'option'))
element.addElement('value', content='value')
option = data_form.Option.fromElement(element)
self.assertEqual('value', option.value)
self.assertIdentical(None, option.label)
def test_fromElementLabel(self):
"""
An option label is an attribute on the option element.
"""
element = domish.Element((NS_X_DATA, 'option'))
element.addElement('value', content='value')
element['label'] = 'label'
option = data_form.Option.fromElement(element)
self.assertEqual('label', option.label)
def test_fromElementNoValue(self):
"""
An option MUST have a value.
"""
element = domish.Element((NS_X_DATA, 'option'))
self.assertRaises(data_form.Error,
data_form.Option.fromElement, element)
def test_repr(self):
"""
The representation of an Option is equal to how it is created.
"""
option = data_form.Option('value', 'label')
self.assertEqual("""Option('value', 'label')""", repr(option))
class FieldTest(unittest.TestCase):
"""
Tests for L{data_form.Field}.
"""
def test_basic(self):
"""
Test basic field initialization.
"""
field = data_form.Field(var='test')
self.assertEqual('text-single', field.fieldType)
self.assertEqual('test', field.var)
def test_labelAndOptions(self):
"""
The label should be set, even if there are options with labels as dict.
"""
field = data_form.Field(label='test',
options={'test2': 'test 2', 'test3': 'test 3'})
self.assertEqual('test', field.label)
def test_repr(self):
"""
The repr of a field should be equal to its initialization.
"""
field = data_form.Field('list-single', var='test', label='label',
desc='desc', required=True, value='test',
options=[data_form.Option('test')])
self.assertEqual("""Field(fieldType='list-single', """
"""var='test', label='label', """
"""desc='desc', required=True, """
"""values=['test'], """
"""options=[Option('test')])""",
repr(field))
def test_toElement(self):
"""
Test rendering to a DOM.
"""
field = data_form.Field(var='test')
element = field.toElement()
self.assertTrue(domish.IElement.providedBy(element))
self.assertEquals('field', element.name)
self.assertEquals(NS_X_DATA, element.uri)
self.assertEquals('text-single',
element.getAttribute('type', 'text-single'))
self.assertEquals('test', element['var'])
self.assertEquals([], element.children)
def test_toElementTypeNotTextSingle(self):
"""
Always render the field type, if different from text-single.
"""
field = data_form.Field('hidden', var='test')
element = field.toElement()
self.assertEquals('hidden', element.getAttribute('type'))
def test_toElementSingleValue(self):
"""
A single value should yield only one value element.
"""
field = data_form.Field('list-multi', var='test', value='test')
element = field.toElement()
children = list(element.elements())
self.assertEqual(1, len(children))
def test_toElementMultipleValues(self):
"""
A field with no type and multiple values should render all values.
"""
field = data_form.Field('list-multi', var='test',
values=['test', 'test2'])
element = field.toElement()
children = list(element.elements())
self.assertEqual(2, len(children))
def test_toElementAsForm(self):
"""
Always render the field type, if asForm is True.
"""
field = data_form.Field(var='test')
element = field.toElement(True)
self.assertEquals('text-single', element.getAttribute('type'))
def test_toElementOptions(self):
"""
Test rendering to a DOM with options.
"""
field = data_form.Field('list-single', var='test')
field.options = [data_form.Option(u'option1'),
data_form.Option(u'option2')]
element = field.toElement(True)
self.assertEqual(2, len(element.children))
def test_toElementLabel(self):
"""
Test rendering to a DOM with a label.
"""
field = data_form.Field(var='test', label=u'my label')
element = field.toElement(True)
self.assertEqual(u'my label', element.getAttribute('label'))
def test_toElementDescription(self):
"""
Test rendering to a DOM with options.
"""
field = data_form.Field(var='test', desc=u'My desc')
element = field.toElement(True)
self.assertEqual(1, len(element.children))
child = element.children[0]
self.assertEqual('desc', child.name)
self.assertEqual(NS_X_DATA, child.uri)
self.assertEqual(u'My desc', unicode(child))
def test_toElementRequired(self):
"""
Test rendering to a DOM with options.
"""
field = data_form.Field(var='test', required=True)
element = field.toElement(True)
self.assertEqual(1, len(element.children))
child = element.children[0]
self.assertEqual('required', child.name)
self.assertEqual(NS_X_DATA, child.uri)
def test_toElementJID(self):
"""
A JID value should render to text.
"""
field = data_form.Field(fieldType='jid-single', var='test',
value=jid.JID(u'test@example.org'))
element = field.toElement()
self.assertEqual(u'test@example.org', unicode(element.value))
def test_toElementJIDTextSingle(self):
"""
A JID value should render to text if field type is text-single.
"""
field = data_form.Field(fieldType='text-single', var='test',
value=jid.JID(u'test@example.org'))
element = field.toElement()
self.assertEqual(u'test@example.org', unicode(element.value))
def test_toElementBoolean(self):
"""
A boolean value should render to text.
"""
field = data_form.Field(fieldType='boolean', var='test',
value=True)
element = field.toElement()
self.assertEqual(u'true', unicode(element.value))
def test_toElementBooleanTextSingle(self):
"""
A boolean value should render to text if the field type is text-single.
"""
field = data_form.Field(var='test', value=True)
element = field.toElement()
self.assertEqual(u'true', unicode(element.value))
def test_toElementNoType(self):
"""
A field with no type should not have a type attribute.
"""
field = data_form.Field(None, var='test', value='test')
element = field.toElement()
self.assertFalse(element.hasAttribute('type'))
def test_toElementNoTypeMultipleValues(self):
"""
A field with no type and multiple values should render all values.
"""
field = data_form.Field(None, var='test', values=['test', 'test2'])
element = field.toElement()
self.assertFalse(element.hasAttribute('type'))
children = list(element.elements())
self.assertEqual(2, len(children))
def test_typeCheckNoFieldName(self):
"""
A field not of type fixed must have a var.
"""
field = data_form.Field(fieldType='list-single')
self.assertRaises(data_form.FieldNameRequiredError, field.typeCheck)
def test_typeCheckTooManyValues(self):
"""
Expect an exception if too many values are set, depending on type.
"""
field = data_form.Field(fieldType='list-single', var='test',
values=[u'value1', u'value2'])
self.assertRaises(data_form.TooManyValuesError, field.typeCheck)
def test_typeCheckBooleanFalse(self):
"""
Test possible False values for a boolean field.
"""
field = data_form.Field(fieldType='boolean', var='test')
for value in (False, 0, '0', 'false', 'False', []):
field.value = value
field.typeCheck()
self.assertIsInstance(field.value, bool)
self.assertFalse(field.value)
def test_typeCheckBooleanTrue(self):
"""
Test possible True values for a boolean field.
"""
field = data_form.Field(fieldType='boolean', var='test')
for value in (True, 1, '1', 'true', 'True', ['test']):
field.value = value
field.typeCheck()
self.assertIsInstance(field.value, bool)
self.assertTrue(field.value)
def test_typeCheckBooleanBad(self):
"""
A bad value for a boolean field should raise a ValueError
"""
field = data_form.Field(fieldType='boolean', var='test')
field.value = 'test'
self.assertRaises(ValueError, field.typeCheck)
def test_typeCheckJID(self):
"""
The value of jid field should be a JID or coercable to one.
"""
field = data_form.Field(fieldType='jid-single', var='test',
value=jid.JID('test@example.org'))
field.typeCheck()
def test_typeCheckJIDString(self):
"""
The string value of jid field should be coercable into a JID.
"""
field = data_form.Field(fieldType='jid-single', var='test',
value='test@example.org')
field.typeCheck()
self.assertEquals(jid.JID(u'test@example.org'), field.value)
def test_typeCheckJIDBad(self):
"""
An invalid JID string should raise an exception.
"""
field = data_form.Field(fieldType='jid-single', var='test',
value='test@@example.org')
self.assertRaises(jid.InvalidFormat, field.typeCheck)
def test_fromElementType(self):
element = domish.Element((NS_X_DATA, 'field'))
element['type'] = 'fixed'
field = data_form.Field.fromElement(element)
self.assertEquals('fixed', field.fieldType)
def test_fromElementNoType(self):
element = domish.Element((NS_X_DATA, 'field'))
field = data_form.Field.fromElement(element)
self.assertEquals(None, field.fieldType)
def test_fromElementValueTextSingle(self):
"""
Parsed text-single field values should be of type C{unicode}.
"""
element = domish.Element((NS_X_DATA, 'field'))
element['type'] = 'text-single'
element.addElement('value', content=u'text')
field = data_form.Field.fromElement(element)
self.assertEquals('text', field.value)
def test_fromElementValueJID(self):
"""
Parsed jid-single field values should be of type C{unicode}.
"""
element = domish.Element((NS_X_DATA, 'field'))
element['type'] = 'jid-single'
element.addElement('value', content=u'user@example.org')
field = data_form.Field.fromElement(element)
self.assertEquals(u'user@example.org', field.value)
def test_fromElementValueJIDMalformed(self):
"""
Parsed jid-single field values should be of type C{unicode}.
No validation should be done at this point, so invalid JIDs should
also be passed as-is.
"""
element = domish.Element((NS_X_DATA, 'field'))
element['type'] = 'jid-single'
element.addElement('value', content=u'@@')
field = data_form.Field.fromElement(element)
self.assertEquals(u'@@', field.value)
def test_fromElementValueBoolean(self):
"""
Parsed boolean field values should be of type C{unicode}.
"""
element = domish.Element((NS_X_DATA, 'field'))
element['type'] = 'boolean'
element.addElement('value', content=u'false')
field = data_form.Field.fromElement(element)
self.assertEquals(u'false', field.value)
def test_fromElementDesc(self):
"""
Field descriptions are in a desc child element.
"""
element = domish.Element((NS_X_DATA, 'field'))
element.addElement('desc', content=u'My description')
field = data_form.Field.fromElement(element)
self.assertEqual(u'My description', field.desc)
def test_fromElementOption(self):
"""
Field descriptions are in a desc child element.
"""
element = domish.Element((NS_X_DATA, 'field'))
element.addElement('option').addElement('value', content=u'option1')
element.addElement('option').addElement('value', content=u'option2')
field = data_form.Field.fromElement(element)
self.assertEqual(2, len(field.options))
def test_fromElementRequired(self):
"""
Required fields have a required child element.
"""
element = domish.Element((NS_X_DATA, 'field'))
element.addElement('required')
field = data_form.Field.fromElement(element)
self.assertTrue(field.required)
def test_fromElementChildOtherNamespace(self):
"""
Child elements from another namespace are ignored.
"""
element = domish.Element((NS_X_DATA, 'field'))
element['var'] = 'test'
element.addElement(('myns', 'value'))
field = data_form.Field.fromElement(element)
self.assertIdentical(None, field.value)
def test_fromDict(self):
"""
A named field with a value can be created by providing a dictionary.
"""
fieldDict = {'var': 'test', 'value': 'text'}
field = data_form.Field.fromDict(fieldDict)
self.assertEqual('test', field.var)
self.assertEqual('text', field.value)
def test_fromDictFieldType(self):
"""
The field type is set using the key 'type'.
"""
fieldDict = {'type': 'boolean'}
field = data_form.Field.fromDict(fieldDict)
self.assertEqual('boolean', field.fieldType)
def test_fromDictOptions(self):
"""
The field options are set using the key 'options'.
The options are represented as a dictionary keyed by option,
with the optional label as value.
"""
fieldDict = {'options': {'value1': 'label1',
'value2': 'label2'}}
field = data_form.Field.fromDict(fieldDict)
self.assertEqual(2, len(field.options))
options = {}
for option in field.options:
options[option.value] = option.label
self.assertEqual(options, fieldDict['options'])
class FormTest(unittest.TestCase):
"""
Tests for L{data_form.Form}.
"""
def test_formType(self):
"""
A form has a type.
"""
form = data_form.Form('result')
self.assertEqual('result', form.formType)
def test_toElement(self):
"""
The toElement method returns a form's DOM representation.
"""
form = data_form.Form('result')
element = form.toElement()
self.assertTrue(domish.IElement.providedBy(element))
self.assertEquals('x', element.name)
self.assertEquals(NS_X_DATA, element.uri)
self.assertEquals('result', element['type'])
self.assertEquals([], element.children)
def test_toElementTitle(self):
"""
A title is rendered as a child element with the title as CDATA.
"""
form = data_form.Form('form', title='Bot configuration')
element = form.toElement()
elements = list(element.elements())
self.assertEqual(1, len(elements))
title = elements[0]
self.assertEqual('title', title.name)
self.assertEqual(NS_X_DATA, title.uri)
self.assertEqual('Bot configuration', unicode(title))
def test_toElementInstructions(self):
"""
Instructions are rendered as child elements with CDATA.
"""
form = data_form.Form('form', instructions=['Fill out this form!'])
element = form.toElement()
elements = list(element.elements())
self.assertEqual(1, len(elements))
instructions = elements[0]
self.assertEqual('instructions', instructions.name)
self.assertEqual(NS_X_DATA, instructions.uri)
self.assertEqual('Fill out this form!', unicode(instructions))
def test_toElementInstructionsMultiple(self):
"""
Instructions render as one element per instruction, in order.
"""
form = data_form.Form('form', instructions=['Fill out this form!',
'no really'])
element = form.toElement()
elements = list(element.elements())
self.assertEqual(2, len(elements))
instructions1 = elements[0]
instructions2 = elements[1]
self.assertEqual('instructions', instructions1.name)
self.assertEqual(NS_X_DATA, instructions1.uri)
self.assertEqual('Fill out this form!', unicode(instructions1))
self.assertEqual('instructions', instructions2.name)
self.assertEqual(NS_X_DATA, instructions2.uri)
self.assertEqual('no really', unicode(instructions2))
def test_toElementFormType(self):
"""
The form type is rendered as a hidden field with name FORM_TYPE.
"""
form = data_form.Form('form', formNamespace='jabber:bot')
element = form.toElement()
elements = list(element.elements())
self.assertEqual(1, len(elements))
formTypeField = elements[0]
self.assertEqual('field', formTypeField.name)
self.assertEqual(NS_X_DATA, formTypeField.uri)
self.assertEqual('FORM_TYPE', formTypeField['var'])
self.assertEqual('hidden', formTypeField['type'])
self.assertEqual('jabber:bot', unicode(formTypeField.value))
def test_toElementFields(self):
"""
Fields are rendered as child elements, in order.
"""
fields = [data_form.Field('fixed', value='Section 1'),
data_form.Field('text-single',
var='botname',
label='The name of your bot'),
data_form.Field('text-multi',
var='description',
label='Helpful description of your bot'),
data_form.Field('boolean',
var='public',
label='Public bot?',
required=True)
]
form = data_form.Form('form', fields=fields)
element = form.toElement()
elements = list(element.elements())
self.assertEqual(4, len(elements))
for field in elements:
self.assertEqual('field', field.name)
self.assertEqual(NS_X_DATA, field.uri)
# Check order
self.assertEqual('fixed', elements[0]['type'])
self.assertEqual('botname', elements[1]['var'])
self.assertEqual('description', elements[2]['var'])
self.assertEqual('public', elements[3]['var'])
def test_fromElement(self):
"""
C{fromElement} creates a L{data_form.Form} from a DOM representation.
"""
element = domish.Element((NS_X_DATA, 'x'))
element['type'] = 'result'
form = data_form.Form.fromElement(element)
self.assertEquals('result', form.formType)
self.assertEquals(None, form.title)
self.assertEquals([], form.instructions)
self.assertEquals({}, form.fields)
def test_fromElementInvalidElementName(self):
"""
Bail if the passed element does not have the correct name.
"""
element = domish.Element((NS_X_DATA, 'form'))
self.assertRaises(Exception, data_form.Form.fromElement, element)
def test_fromElementInvalidElementURI(self):
"""
Bail if the passed element does not have the correct namespace.
"""
element = domish.Element(('myns', 'x'))
self.assertRaises(Exception, data_form.Form.fromElement, element)
def test_fromElementTitle(self):
element = domish.Element((NS_X_DATA, 'x'))
element.addElement('title', content='My title')
form = data_form.Form.fromElement(element)
self.assertEquals('My title', form.title)
def test_fromElementInstructions(self):
element = domish.Element((NS_X_DATA, 'x'))
element.addElement('instructions', content='instruction')
form = data_form.Form.fromElement(element)
self.assertEquals(['instruction'], form.instructions)
def test_fromElementInstructions2(self):
element = domish.Element((NS_X_DATA, 'x'))
element.addElement('instructions', content='instruction 1')
element.addElement('instructions', content='instruction 2')
form = data_form.Form.fromElement(element)
self.assertEquals(['instruction 1', 'instruction 2'], form.instructions)
def test_fromElementOneField(self):
element = domish.Element((NS_X_DATA, 'x'))
element.addElement('field')
form = data_form.Form.fromElement(element)
self.assertEquals(1, len(form.fieldList))
self.assertNotIn('field', form.fields)
def test_fromElementTwoFields(self):
element = domish.Element((NS_X_DATA, 'x'))
element.addElement('field')['var'] = 'field1'
element.addElement('field')['var'] = 'field2'
form = data_form.Form.fromElement(element)
self.assertEquals(2, len(form.fieldList))
self.assertIn('field1', form.fields)
self.assertEquals('field1', form.fieldList[0].var)
self.assertIn('field2', form.fields)
self.assertEquals('field2', form.fieldList[1].var)
def test_fromElementFormType(self):
"""
The form type is a hidden field named FORM_TYPE.
"""
element = domish.Element((NS_X_DATA, 'x'))
field = element.addElement('field')
field['var'] = 'FORM_TYPE'
field['type'] = 'hidden'
field.addElement('value', content='myns')
form = data_form.Form.fromElement(element)
self.assertNotIn('FORM_TYPE', form.fields)
self.assertEqual('myns', form.formNamespace)
def test_fromElementFormTypeNotHidden(self):
"""
A non-hidden field named FORM_TYPE does not set the form type.
"""
element = domish.Element((NS_X_DATA, 'x'))
field = element.addElement('field')
field['var'] = 'FORM_TYPE'
field.addElement('value', content='myns')
form = data_form.Form.fromElement(element)
self.assertIn('FORM_TYPE', form.fields)
self.assertIdentical(None, form.formNamespace)
def test_fromElementChildOtherNamespace(self):
"""
Child elements from another namespace are ignored.
"""
element = domish.Element((NS_X_DATA, 'x'))
element['type'] = 'result'
field = element.addElement(('myns', 'field'))
field['var'] = 'test'
form = data_form.Form.fromElement(element)
self.assertEqual(0, len(form.fields))
def test_repr(self):
"""
The repr of a form should be equal to its initialization.
"""
form = data_form.Form('form', title='title', instructions=['instr'],
formNamespace='myns',
fields=[data_form.Field('fixed',
value='test')])
self.assertEqual("""Form(formType='form', title='title', """
"""instructions=['instr'], formNamespace='myns', """
"""fields=[Field(fieldType='fixed', """
"""values=['test'])])""",
repr(form))
def test_addField(self):
"""
A field should occur in fieldList.
"""
form = data_form.Form('result')
field = data_form.Field('fixed', value='Section 1')
form.addField(field)
self.assertEqual([field], form.fieldList)
def test_addFieldTwice(self):
"""
Fields occur in fieldList in the order they were added.
"""
form = data_form.Form('result')
field1 = data_form.Field('fixed', value='Section 1')
field2 = data_form.Field('fixed', value='Section 2')
form.addField(field1)
form.addField(field2)
self.assertEqual([field1, field2], form.fieldList)
def test_addFieldNotNamed(self):
"""
A non-named field should not occur in fields.
"""
form = data_form.Form('result')
field = data_form.Field('fixed', value='Section 1')
form.addField(field)
self.assertEqual({}, form.fields)
def test_addFieldNamed(self):
"""
A named field should occur in fields.
"""
form = data_form.Form('result')
field = data_form.Field(var='test')
form.addField(field)
self.assertEqual({'test': field}, form.fields)
def test_addFieldTwiceNamed(self):
"""
A second named field should occur in fields.
"""
form = data_form.Form('result')
field1 = data_form.Field(var='test')
field2 = data_form.Field(var='test2')
form.addField(field2)
form.addField(field1)
self.assertEqual({'test': field1, 'test2': field2}, form.fields)
def test_addFieldSameName(self):
"""
A named field cannot occur twice.
"""
form = data_form.Form('result')
field1 = data_form.Field(var='test', value='value')
field2 = data_form.Field(var='test', value='value2')
form.addField(field1)
self.assertRaises(data_form.Error, form.addField, field2)
def test_removeField(self):
"""
A removed field should not occur in fieldList.
"""
form = data_form.Form('result')
field = data_form.Field('fixed', value='Section 1')
form.addField(field)
form.removeField(field)
self.assertNotIn(field, form.fieldList)
def test_removeFieldNamed(self):
"""
A removed named field should not occur in fields.
"""
form = data_form.Form('result')
field = data_form.Field(var='test', value='test1')
form.addField(field)
form.removeField(field)
self.assertNotIn('test', form.fields)
def test_makeField(self):
"""
Fields can be created from a dict of values and a dict of field defs.
"""
fieldDefs = {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
"pubsub#deliver_payloads":
{"type": "boolean",
"label": "Deliver payloads with event notifications"},
"pubsub#creator":
{"type": "jid-single",
"label": "The JID of the node creator"},
"pubsub#description":
{"type": "text-single",
"label": "A description of the node"},
"pubsub#owner":
{"type": "jid-single",
"label": "Owner of the node"},
}
values = {'pubsub#deliver_payloads': '0',
'pubsub#persist_items': True,
'pubsub#description': 'a great node',
'pubsub#owner': jid.JID('user@example.org'),
'x-myfield': ['a', 'b']}
form = data_form.Form('submit')
form.makeFields(values, fieldDefs)
# Check that the expected fields have been created
self.assertIn('pubsub#deliver_payloads', form.fields)
self.assertIn('pubsub#persist_items', form.fields)
self.assertIn('pubsub#description', form.fields)
self.assertIn('pubsub#owner', form.fields)
# This field is not created because there is no value for it.
self.assertNotIn('pubsub#creator', form.fields)
# This field is not created because it does not appear in fieldDefs
# and filterUnknown defaults to True
self.assertNotIn('x-myfield', form.fields)
# Check properties the created fields
self.assertEqual('boolean',
form.fields['pubsub#deliver_payloads'].fieldType)
self.assertEqual('0',
form.fields['pubsub#deliver_payloads'].value)
self.assertEqual('Deliver payloads with event notifications',
form.fields['pubsub#deliver_payloads'].label)
self.assertEqual(True,
form.fields['pubsub#persist_items'].value)
def test_makeFieldNotFilterUnknown(self):
"""
Fields can be created from a dict of values and a dict of field defs.
"""
fieldDefs = {
"pubsub#persist_items":
{"type": "boolean",
"label": "Persist items to storage"},
}
values = {'x-myfield': ['a', 'b']}
form = data_form.Form('submit')
form.makeFields(values, fieldDefs, filterUnknown=False)
field = form.fields['x-myfield']
self.assertEqual(None, field.fieldType)
self.assertEqual(values, form.getValues())
def test_makeFieldsUnknownTypeJID(self):
"""
Without type, a single JID value sets field type jid-single.
"""
values = {'pubsub#creator': jid.JID('user@example.org')}
form = data_form.Form('result')
form.makeFields(values)
field = form.fields['pubsub#creator']
self.assertEqual(None, field.fieldType)
self.assertEqual(values, form.getValues())
def test_makeFieldsUnknownTypeJIDMulti(self):
"""
Without type, multiple JID values sets field type jid-multi.
"""
values = {'pubsub#contact': [jid.JID('user@example.org'),
jid.JID('other@example.org')]}
form = data_form.Form('result')
form.makeFields(values)
field = form.fields['pubsub#contact']
self.assertEqual(None, field.fieldType)
self.assertEqual(values, form.getValues())
def test_makeFieldsUnknownTypeBoolean(self):
"""
Without type, a boolean value sets field type boolean.
"""
values = {'pubsub#persist_items': True}
form = data_form.Form('result')
form.makeFields(values)
field = form.fields['pubsub#persist_items']
self.assertEqual(None, field.fieldType)
self.assertEqual(values, form.getValues())
def test_makeFieldsUnknownTypeListMulti(self):
"""
Without type, multiple values sets field type list-multi.
"""
values = {'pubsub#show-values': ['chat', 'online', 'away']}
form = data_form.Form('result')
form.makeFields(values)
field = form.fields['pubsub#show-values']
self.assertEqual(None, field.fieldType)
self.assertEqual(values, form.getValues())
def test_interface(self):
"""
L{Form}s act as a read-only dictionary.
"""
form = data_form.Form('submit')
verify.verifyObject(IIterableMapping, form)
def test_getitem(self):
"""
Using Form as a mapping will yield the value of fields keyed by name.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True),
data_form.Field('list-multi', var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual('The Jabber Bot', form['botname'])
self.assertTrue(form['public'])
self.assertEqual(['news', 'search'], form['features'])
def test_getitemOneValueTypeMulti(self):
"""
A single value for a multi-value field type is returned in a list.
"""
fields = [data_form.Field('list-multi', var='features',
values=['news'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual(['news'], form['features'])
def test_getitemMultipleValuesNoType(self):
"""
Multiple values for a field without type are returned in a list.
"""
fields = [data_form.Field(None, var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual(['news', 'search'], form['features'])
def test_getitemMultipleValuesTypeSingle(self):
"""
Multiple values for a single-value field type returns the first value.
"""
fields = [data_form.Field('text-single', var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual('news', form['features'])
def test_get(self):
"""
Getting the value of a known field succeeds.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot')]
form = data_form.Form('submit', fields=fields)
self.assertEqual('The Jabber Bot', form.get('botname'))
def test_getUnknownNone(self):
"""
Getting the value of a unknown field returns None.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot')]
form = data_form.Form('submit', fields=fields)
self.assertIdentical(None, form.get('features'))
def test_getUnknownDefault(self):
"""
Getting the value of a unknown field returns specified default.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot')]
form = data_form.Form('submit', fields=fields)
self.assertTrue(form.get('public', True))
def test_contains(self):
"""
A form contains a known field.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot')]
form = data_form.Form('submit', fields=fields)
self.assertIn('botname', form)
def test_containsNot(self):
"""
A form does not contains an unknown field.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot')]
form = data_form.Form('submit', fields=fields)
self.assertNotIn('features', form)
def test_iterkeys(self):
"""
Iterating over the keys of a form yields all field names.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True),
data_form.Field('list-multi', var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual(set(['botname', 'public', 'features']),
set(form.iterkeys()))
def test_itervalues(self):
"""
Iterating over the values of a form yields all field values.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True)]
form = data_form.Form('submit', fields=fields)
self.assertEqual(set(['The Jabber Bot', True]),
set(form.itervalues()))
def test_iteritems(self):
"""
Iterating over the values of a form yields all item tuples.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True)]
form = data_form.Form('submit', fields=fields)
self.assertEqual(set([('botname', 'The Jabber Bot'),
('public', True)]),
set(form.iteritems()))
def test_keys(self):
"""
Getting the keys of a form yields a list of field names.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True),
data_form.Field('list-multi', var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
keys = form.keys()
self.assertIsInstance(keys, list)
self.assertEqual(set(['botname', 'public', 'features']),
set(keys))
def test_values(self):
"""
Getting the values of a form yields a list of field values.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True)]
form = data_form.Form('submit', fields=fields)
values = form.values()
self.assertIsInstance(values, list)
self.assertEqual(set(['The Jabber Bot', True]), set(values))
def test_items(self):
"""
Iterating over the values of a form yields a list of all item tuples.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True)]
form = data_form.Form('submit', fields=fields)
items = form.items()
self.assertIsInstance(items, list)
self.assertEqual(set([('botname', 'The Jabber Bot'),
('public', True)]),
set(items))
def test_getValues(self):
"""
L{Form.getValues} returns a dict of all field values.
"""
fields = [data_form.Field(var='botname', value='The Jabber Bot'),
data_form.Field('boolean', var='public', value=True),
data_form.Field('list-multi', var='features',
values=['news', 'search'])]
form = data_form.Form('submit', fields=fields)
self.assertEqual({'botname': 'The Jabber Bot',
'public': True,
'features': ['news', 'search']},
form.getValues())
def test_typeCheckKnownFieldChecked(self):
"""
Known fields are type checked.
"""
checked = []
fieldDefs = {"pubsub#description":
{"type": "text-single",
"label": "A description of the node"}}
form = data_form.Form('submit')
form.addField(data_form.Field(var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs)
self.assertEqual([None], checked)
def test_typeCheckKnownFieldNoType(self):
"""
Known fields without a type get the type of the field definition.
"""
checked = []
fieldDefs = {"pubsub#description":
{"type": "text-single",
"label": "A description of the node"}}
form = data_form.Form('submit')
form.addField(data_form.Field(None, var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs)
self.assertEqual('text-single', field.fieldType)
self.assertEqual([None], checked)
def test_typeCheckWrongFieldType(self):
"""
A field should have the same type as the field definition.
"""
checked = []
fieldDefs = {"pubsub#description":
{"type": "text-single",
"label": "A description of the node"}}
form = data_form.Form('submit')
form.addField(data_form.Field('list-single', var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
self.assertRaises(TypeError, form.typeCheck, fieldDefs)
self.assertEqual([], checked)
def test_typeCheckDefaultTextSingle(self):
"""
If a field definition has no type, use text-single.
"""
checked = []
fieldDefs = {"pubsub#description":
{"label": "A description of the node"}}
form = data_form.Form('submit')
form.addField(data_form.Field('text-single', var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs)
self.assertEqual([None], checked)
def test_typeCheckUnknown(self):
"""
Unknown fields are checked, not removed if filterUnknown False.
"""
checked = []
fieldDefs = {}
form = data_form.Form('submit')
form.addField(data_form.Field('list-single', var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs, filterUnknown=False)
self.assertIn('pubsub#description', form.fields)
self.assertEqual([None], checked)
def test_typeCheckUnknownNoType(self):
"""
Unknown fields without type are not checked.
"""
checked = []
fieldDefs = {}
form = data_form.Form('submit')
form.addField(data_form.Field(None, var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs, filterUnknown=False)
self.assertIn('pubsub#description', form.fields)
self.assertEqual([], checked)
def test_typeCheckUnknownRemoved(self):
"""
Unknown fields are not checked, and removed if filterUnknown True.
"""
checked = []
fieldDefs = {}
form = data_form.Form('submit')
form.addField(data_form.Field('list-single', var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck(fieldDefs, filterUnknown=True)
self.assertNotIn('pubsub#description', form.fields)
self.assertEqual([], checked)
class FindFormTest(unittest.TestCase):
"""
Tests for L{data_form.findForm}.
"""
def test_findForm(self):
element = domish.Element((None, 'test'))
theForm = data_form.Form('submit', formNamespace='myns')
element.addChild(theForm.toElement())
form = data_form.findForm(element, 'myns')
self.assertEqual('myns', form.formNamespace)
def test_noFormType(self):
element = domish.Element((None, 'test'))
otherForm = data_form.Form('submit')
element.addChild(otherForm.toElement())
form = data_form.findForm(element, 'myns')
self.assertIdentical(None, form)
def test_noFormTypeCancel(self):
"""
Cancelled forms don't have a FORM_TYPE field, the first is returned.
"""
element = domish.Element((None, 'test'))
cancelledForm = data_form.Form('cancel')
element.addChild(cancelledForm.toElement())
form = data_form.findForm(element, 'myns')
self.assertEqual('cancel', form.formType)
def test_otherFormType(self):
"""
Forms with other FORM_TYPEs are ignored.
"""
element = domish.Element((None, 'test'))
otherForm = data_form.Form('submit', formNamespace='otherns')
element.addChild(otherForm.toElement())
form = data_form.findForm(element, 'myns')
self.assertIdentical(None, form)
def test_otherFormTypeCancel(self):
"""
Cancelled forms with another FORM_TYPE are ignored.
"""
element = domish.Element((None, 'test'))
cancelledForm = data_form.Form('cancel', formNamespace='otherns')
element.addChild(cancelledForm.toElement())
form = data_form.findForm(element, 'myns')
self.assertIdentical(None, form)
def test_noElement(self):
"""
When None is passed as element, None is returned.
"""
element = None
form = data_form.findForm(element, 'myns')
self.assertIdentical(None, form)
def test_noForm(self):
"""
When no child element is a form, None is returned.
"""
element = domish.Element((None, 'test'))
form = data_form.findForm(element, 'myns')
self.assertIdentical(None, form)
def test_typeCheckNoFieldDefs(self):
"""
If there are no field defs, an empty dictionary is assumed.
"""
checked = []
form = data_form.Form('submit')
form.addField(data_form.Field('list-single', var='pubsub#description',
value='a node'))
field = form.fields['pubsub#description']
field.typeCheck = lambda : checked.append(None)
form.typeCheck()
self.assertIn('pubsub#description', form.fields)
self.assertEqual([None], checked)
wokkel-0.7.1/wokkel/test/__init__.py 0000775 0001750 0001750 00000000127 11707215356 020203 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel}.
"""
wokkel-0.7.1/wokkel/test/test_subprotocols.py 0000775 0001750 0001750 00000074214 12074262111 022236 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.subprotocols}
"""
from zope.interface.verify import verifyObject
from twisted.trial import unittest
from twisted.test import proto_helpers
from twisted.internet import defer, task
from twisted.internet.error import ConnectionDone
from twisted.python import failure
from twisted.words.xish import domish
from twisted.words.protocols.jabber import error, ijabber, xmlstream
from wokkel import generic, subprotocols
class DeprecationTest(unittest.TestCase):
"""
Deprecation test for L{wokkel.subprotocols}.
"""
def lookForDeprecationWarning(self, testmethod, attributeName, newName):
"""
Importing C{testmethod} emits a deprecation warning.
"""
warningsShown = self.flushWarnings([testmethod])
self.assertEqual(len(warningsShown), 1)
self.assertIdentical(warningsShown[0]['category'], DeprecationWarning)
self.assertEqual(
warningsShown[0]['message'],
"wokkel.subprotocols." + attributeName + " "
"was deprecated in Wokkel 0.7.0: Use " + newName + " instead.")
def test_xmppHandlerCollection(self):
"""
L{subprotocols.XMPPHandlerCollection} is deprecated.
"""
from wokkel.subprotocols import XMPPHandlerCollection
XMPPHandlerCollection
self.lookForDeprecationWarning(
self.test_xmppHandlerCollection,
"XMPPHandlerCollection",
"twisted.words.protocols.jabber.xmlstream."
"XMPPHandlerCollection")
class DummyFactory(object):
"""
Dummy XmlStream factory that only registers bootstrap observers.
"""
def __init__(self):
self.callbacks = {}
def addBootstrap(self, event, callback):
self.callbacks[event] = callback
class DummyXMPPHandler(subprotocols.XMPPHandler):
"""
Dummy XMPP subprotocol handler to count the methods are called on it.
"""
def __init__(self):
self.doneMade = 0
self.doneInitialized = 0
self.doneLost = 0
def makeConnection(self, xs):
self.connectionMade()
def connectionMade(self):
self.doneMade += 1
def connectionInitialized(self):
self.doneInitialized += 1
def connectionLost(self, reason):
self.doneLost += 1
class FailureReasonXMPPHandler(subprotocols.XMPPHandler):
"""
Dummy handler specifically for failure Reason tests.
"""
def __init__(self):
self.gotFailureReason = False
def connectionLost(self, reason):
if isinstance(reason, failure.Failure):
self.gotFailureReason = True
class IQGetStanza(generic.Stanza):
timeout = None
stanzaKind = 'iq'
stanzaType = 'get'
stanzaID = 'test'
class XMPPHandlerTest(unittest.TestCase):
"""
Tests for L{subprotocols.XMPPHandler}.
"""
def test_interface(self):
"""
L{xmlstream.XMPPHandler} implements L{ijabber.IXMPPHandler}.
"""
verifyObject(ijabber.IXMPPHandler, subprotocols.XMPPHandler())
def test_send(self):
"""
Test that data is passed on for sending by the stream manager.
"""
class DummyStreamManager(object):
def __init__(self):
self.outlist = []
def send(self, data):
self.outlist.append(data)
handler = subprotocols.XMPPHandler()
handler.parent = DummyStreamManager()
handler.send('')
self.assertEquals([''], handler.parent.outlist)
def test_makeConnection(self):
"""
Test that makeConnection saves the XML stream and calls connectionMade.
"""
class TestXMPPHandler(subprotocols.XMPPHandler):
def connectionMade(self):
self.doneMade = True
handler = TestXMPPHandler()
xs = xmlstream.XmlStream(xmlstream.Authenticator())
handler.makeConnection(xs)
self.assertTrue(handler.doneMade)
self.assertIdentical(xs, handler.xmlstream)
def test_connectionLost(self):
"""
Test that connectionLost forgets the XML stream.
"""
handler = subprotocols.XMPPHandler()
xs = xmlstream.XmlStream(xmlstream.Authenticator())
handler.makeConnection(xs)
handler.connectionLost(Exception())
self.assertIdentical(None, handler.xmlstream)
def test_request(self):
"""
A request is passed up to the stream manager.
"""
class DummyStreamManager(object):
def __init__(self):
self.requests = []
def request(self, request):
self.requests.append(request)
return defer.succeed(None)
handler = subprotocols.XMPPHandler()
handler.parent = DummyStreamManager()
request = IQGetStanza()
d = handler.request(request)
self.assertEquals(1, len(handler.parent.requests))
self.assertIdentical(request, handler.parent.requests[-1])
return d
class StreamManagerTest(unittest.TestCase):
"""
Tests for L{subprotocols.StreamManager}.
"""
def setUp(self):
factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
self.clock = task.Clock()
self.streamManager = subprotocols.StreamManager(factory, self.clock)
self.xmlstream = factory.buildProtocol(None)
self.transport = proto_helpers.StringTransport()
self.xmlstream.transport = self.transport
self.request = IQGetStanza()
def _streamStarted(self):
"""
Bring the test stream to the initialized state.
"""
self.xmlstream.connectionMade()
self.xmlstream.dataReceived(
"")
self.xmlstream.dispatch(self.xmlstream, "//event/stream/authd")
def test_basic(self):
"""
Test correct initialization and setup of factory observers.
"""
factory = DummyFactory()
sm = subprotocols.StreamManager(factory)
self.assertIdentical(None, sm.xmlstream)
self.assertEquals([], sm.handlers)
self.assertEquals(sm._connected,
sm.factory.callbacks['//event/stream/connected'])
self.assertEquals(sm._authd,
sm.factory.callbacks['//event/stream/authd'])
self.assertEquals(sm._disconnected,
sm.factory.callbacks['//event/stream/end'])
self.assertEquals(sm.initializationFailed,
sm.factory.callbacks['//event/xmpp/initfailed'])
def test_connected(self):
"""
Test that protocol handlers have their connectionMade method called
when the XML stream is connected.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._connected(xs)
self.assertEquals(1, handler.doneMade)
self.assertEquals(0, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
def test_connectedLogTrafficFalse(self):
"""
Test raw data functions unset when logTraffic is set to False.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._connected(xs)
self.assertIdentical(None, xs.rawDataInFn)
self.assertIdentical(None, xs.rawDataOutFn)
def test_connectedLogTrafficTrue(self):
"""
Test raw data functions set when logTraffic is set to True.
"""
sm = self.streamManager
sm.logTraffic = True
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._connected(xs)
self.assertNotIdentical(None, xs.rawDataInFn)
self.assertNotIdentical(None, xs.rawDataOutFn)
def test_authd(self):
"""
Test that protocol handlers have their connectionInitialized method
called when the XML stream is initialized.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._authd(xs)
self.assertEquals(0, handler.doneMade)
self.assertEquals(1, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
def test_disconnected(self):
"""
Protocol handlers have connectionLost called on stream disconnect.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
sm._disconnected(None)
self.assertEquals(0, handler.doneMade)
self.assertEquals(0, handler.doneInitialized)
self.assertEquals(1, handler.doneLost)
def test_disconnectedReason(self):
"""
A L{STREAM_END_EVENT} results in L{StreamManager} firing the handlers
L{connectionLost} methods, passing a L{failure.Failure} reason.
"""
sm = self.streamManager
handler = FailureReasonXMPPHandler()
handler.setHandlerParent(sm)
xmlstream.XmlStream(xmlstream.Authenticator())
sm._disconnected(failure.Failure(Exception("no reason")))
self.assertEquals(True, handler.gotFailureReason)
def test_addHandler(self):
"""
Test the addition of a protocol handler while not connected.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
self.assertEquals(0, handler.doneMade)
self.assertEquals(0, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
def test_addHandlerConnected(self):
"""
Adding a handler when connected doesn't call connectionInitialized.
"""
sm = self.streamManager
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._connected(xs)
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
self.assertEquals(1, handler.doneMade)
self.assertEquals(0, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
def test_addHandlerConnectedNested(self):
"""
Adding a handler in connectionMade doesn't cause 2nd call.
"""
class NestingHandler(DummyXMPPHandler):
nestedHandler = None
def connectionMade(self):
DummyXMPPHandler.connectionMade(self)
self.nestedHandler = DummyXMPPHandler()
self.nestedHandler.setHandlerParent(self.parent)
sm = self.streamManager
xs = xmlstream.XmlStream(xmlstream.Authenticator())
handler = NestingHandler()
handler.setHandlerParent(sm)
sm._connected(xs)
self.assertEquals(1, handler.doneMade)
self.assertEquals(0, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
self.assertEquals(1, handler.nestedHandler.doneMade)
self.assertEquals(0, handler.nestedHandler.doneInitialized)
self.assertEquals(0, handler.nestedHandler.doneLost)
def test_addHandlerInitialized(self):
"""
Test the addition of a protocol handler after the stream
have been initialized.
Make sure that the handler will have the connected stream
passed via C{makeConnection} and have C{connectionInitialized}
called.
"""
sm = self.streamManager
xs = xmlstream.XmlStream(xmlstream.Authenticator())
sm._connected(xs)
sm._authd(xs)
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
self.assertEquals(1, handler.doneMade)
self.assertEquals(1, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
def test_addHandlerInitializedNested(self):
"""
Adding a handler in connectionInitialized doesn't cause 2nd call.
"""
class NestingHandler(DummyXMPPHandler):
nestedHandler = None
def connectionInitialized(self):
DummyXMPPHandler.connectionInitialized(self)
self.nestedHandler = DummyXMPPHandler()
self.nestedHandler.setHandlerParent(self.parent)
sm = self.streamManager
xs = xmlstream.XmlStream(xmlstream.Authenticator())
handler = NestingHandler()
handler.setHandlerParent(sm)
sm._connected(xs)
sm._authd(xs)
self.assertEquals(1, handler.doneMade)
self.assertEquals(1, handler.doneInitialized)
self.assertEquals(0, handler.doneLost)
self.assertEquals(1, handler.nestedHandler.doneMade)
self.assertEquals(1, handler.nestedHandler.doneInitialized)
self.assertEquals(0, handler.nestedHandler.doneLost)
def test_addHandlerConnectionLostNested(self):
"""
Adding a handler in connectionLost doesn't call connectionLost there.
"""
class NestingHandler(DummyXMPPHandler):
nestedHandler = None
def connectionLost(self, reason):
DummyXMPPHandler.connectionLost(self, reason)
self.nestedHandler = DummyXMPPHandler()
self.nestedHandler.setHandlerParent(self.parent)
sm = self.streamManager
xs = xmlstream.XmlStream(xmlstream.Authenticator())
handler = NestingHandler()
handler.setHandlerParent(sm)
sm._connected(xs)
sm._authd(xs)
sm._disconnected(xs)
self.assertEquals(1, handler.doneMade)
self.assertEquals(1, handler.doneInitialized)
self.assertEquals(1, handler.doneLost)
self.assertEquals(0, handler.nestedHandler.doneMade)
self.assertEquals(0, handler.nestedHandler.doneInitialized)
self.assertEquals(0, handler.nestedHandler.doneLost)
def test_removeHandler(self):
"""
Test removal of protocol handler.
"""
sm = self.streamManager
handler = DummyXMPPHandler()
handler.setHandlerParent(sm)
handler.disownHandlerParent(sm)
self.assertNotIn(handler, sm)
self.assertIdentical(None, handler.parent)
def test_sendInitialized(self):
"""
Test send when the stream has been initialized.
The data should be sent directly over the XML stream.
"""
factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
sm = subprotocols.StreamManager(factory)
xs = factory.buildProtocol(None)
xs.transport = proto_helpers.StringTransport()
xs.connectionMade()
xs.dataReceived("")
xs.dispatch(xs, "//event/stream/authd")
sm.send("")
self.assertEquals("", xs.transport.value())
def test_sendNotConnected(self):
"""
Test send when there is no established XML stream.
The data should be cached until an XML stream has been established and
initialized.
"""
factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
sm = subprotocols.StreamManager(factory)
handler = DummyXMPPHandler()
sm.addHandler(handler)
xs = factory.buildProtocol(None)
xs.transport = proto_helpers.StringTransport()
sm.send("")
self.assertEquals("", xs.transport.value())
self.assertEquals("", sm._packetQueue[0])
xs.connectionMade()
self.assertEquals("", xs.transport.value())
self.assertEquals("", sm._packetQueue[0])
xs.dataReceived("")
xs.dispatch(xs, "//event/stream/authd")
self.assertEquals("", xs.transport.value())
self.assertFalse(sm._packetQueue)
def test_sendNotInitialized(self):
"""
Test send when the stream is connected but not yet initialized.
The data should be cached until the XML stream has been initialized.
"""
factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
sm = subprotocols.StreamManager(factory)
xs = factory.buildProtocol(None)
xs.transport = proto_helpers.StringTransport()
xs.connectionMade()
xs.dataReceived("")
sm.send("")
self.assertEquals("", xs.transport.value())
self.assertEquals("", sm._packetQueue[0])
def test_sendDisconnected(self):
"""
Test send after XML stream disconnection.
The data should be cached until a new XML stream has been established
and initialized.
"""
factory = xmlstream.XmlStreamFactory(xmlstream.Authenticator())
sm = subprotocols.StreamManager(factory)
handler = DummyXMPPHandler()
sm.addHandler(handler)
xs = factory.buildProtocol(None)
xs.connectionMade()
xs.transport = proto_helpers.StringTransport()
xs.connectionLost(None)
sm.send("")
self.assertEquals("", xs.transport.value())
self.assertEquals("", sm._packetQueue[0])
def test_requestSendInitialized(self):
"""
A request is sent out over the wire when the stream is initialized.
"""
self._streamStarted()
self.streamManager.request(self.request)
expected = u"" % self.request.stanzaID
self.assertEquals(expected, self.transport.value())
def test_requestSendInitializedFreshID(self):
"""
A request without an ID gets a fresh one upon send.
"""
self._streamStarted()
self.request.stanzaID = None
self.streamManager.request(self.request)
self.assertNotIdentical(None, self.request.stanzaID)
expected = u"" % self.request.stanzaID
self.assertEquals(expected, self.transport.value())
def test_requestSendNotConnected(self):
"""
A request is queued until a stream is initialized.
"""
handler = DummyXMPPHandler()
self.streamManager.addHandler(handler)
self.streamManager.request(self.request)
expected = u""
xs = self.xmlstream
self.assertEquals("", xs.transport.value())
xs.connectionMade()
self.assertEquals("", xs.transport.value())
xs.dataReceived("")
xs.dispatch(xs, "//event/stream/authd")
self.assertEquals(expected, xs.transport.value())
self.assertFalse(self.streamManager._packetQueue)
def test_requestResultResponse(self):
"""
A result response gets the request deferred fired with the response.
"""
def cb(result):
self.assertEquals(result['type'], 'result')
self._streamStarted()
d = self.streamManager.request(self.request)
d.addCallback(cb)
xs = self.xmlstream
xs.dataReceived("")
return d
def test_requestErrorResponse(self):
"""
An error response gets the request deferred fired with a failure.
"""
self._streamStarted()
d = self.streamManager.request(self.request)
self.assertFailure(d, error.StanzaError)
xs = self.xmlstream
xs.dataReceived("")
return d
def test_requestNonTrackedResponse(self):
"""
Test that untracked iq responses don't trigger any action.
Untracked means that the id of the incoming response iq is not
in the stream's C{iqDeferreds} dictionary.
"""
# Set up a fallback handler that checks the stanza's handled attribute.
# If that is set to True, the iq tracker claims to have handled the
# response.
dispatched = []
def cb(iq):
dispatched.append(iq)
self._streamStarted()
self.xmlstream.addObserver("/iq", cb, -1)
# Receive an untracked iq response
self.xmlstream.dataReceived("")
self.assertEquals(1, len(dispatched))
self.assertFalse(getattr(dispatched[-1], 'handled', False))
def test_requestCleanup(self):
"""
Test if the deferred associated with an iq request is removed
from the list kept in the L{XmlStream} object after it has
been fired.
"""
self._streamStarted()
d = self.streamManager.request(self.request)
xs = self.xmlstream
xs.dataReceived("")
self.assertNotIn('test', self.streamManager._iqDeferreds)
return d
def test_requestDisconnectCleanup(self):
"""
Test if deferreds for iq's that haven't yet received a response
have their errback called on stream disconnect.
"""
d = self.streamManager.request(self.request)
xs = self.xmlstream
xs.connectionLost(failure.Failure(ConnectionDone()))
self.assertFailure(d, ConnectionDone)
return d
def test_requestNoModifyingDict(self):
"""
Test to make sure the errbacks cannot cause the iteration of the
iqDeferreds to blow up in our face.
"""
def eb(failure):
d = xmlstream.IQ(self.xmlstream).send()
d.addErrback(eb)
d = self.streamManager.request(self.request)
d.addErrback(eb)
self.xmlstream.connectionLost(failure.Failure(ConnectionDone()))
return d
def test_requestTimingOut(self):
"""
Test that an iq request with a defined timeout times out.
"""
self.request.timeout = 60
d = self.streamManager.request(self.request)
self.assertFailure(d, xmlstream.TimeoutError)
self.clock.pump([1, 60])
self.assertFalse(self.clock.calls)
self.assertFalse(self.streamManager._iqDeferreds)
return d
def test_requestNotTimingOut(self):
"""
Test that an iq request with a defined timeout does not time out
when a response was received before the timeout period elapsed.
"""
self._streamStarted()
self.request.timeout = 60
d = self.streamManager.request(self.request)
self.clock.callLater(1, self.xmlstream.dataReceived,
"")
self.clock.pump([1, 1])
self.assertFalse(self.clock.calls)
return d
def test_requestDisconnectTimeoutCancellation(self):
"""
Test if timeouts for iq's that haven't yet received a response
are cancelled on stream disconnect.
"""
self.request.timeout = 60
d = self.streamManager.request(self.request)
self.xmlstream.connectionLost(failure.Failure(ConnectionDone()))
self.assertFailure(d, ConnectionDone)
self.assertFalse(self.clock.calls)
return d
def test_requestNotIQ(self):
"""
The request stanza must be an iq.
"""
stanza = generic.Stanza()
stanza.stanzaKind = 'message'
d = self.streamManager.request(stanza)
self.assertFailure(d, ValueError)
def test_requestNotResult(self):
"""
The request stanza cannot be of type 'result'.
"""
stanza = generic.Stanza()
stanza.stanzaKind = 'iq'
stanza.stanzaType = 'result'
d = self.streamManager.request(stanza)
self.assertFailure(d, ValueError)
def test_requestNotError(self):
"""
The request stanza cannot be of type 'error'.
"""
stanza = generic.Stanza()
stanza.stanzaKind = 'iq'
stanza.stanzaType = 'error'
d = self.streamManager.request(stanza)
self.assertFailure(d, ValueError)
class DummyIQHandler(subprotocols.IQHandlerMixin):
iqHandlers = {'/iq[@type="get"]': 'onGet'}
def __init__(self):
self.output = []
self.xmlstream = xmlstream.XmlStream(xmlstream.Authenticator())
self.xmlstream.send = self.output.append
def send(self, obj):
self.xmlstream.send(obj)
class IQHandlerTest(unittest.TestCase):
def test_match(self):
"""
Test that the matching handler gets called.
"""
class Handler(DummyIQHandler):
called = False
def onGet(self, iq):
self.called = True
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
self.assertTrue(handler.called)
def test_noMatch(self):
"""
Test that the matching handler gets called.
"""
class Handler(DummyIQHandler):
called = False
def onGet(self, iq):
self.called = True
iq = domish.Element((None, 'iq'))
iq['type'] = 'set'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
self.assertFalse(handler.called)
def test_success(self):
"""
Test response when the request is handled successfully.
"""
class Handler(DummyIQHandler):
def onGet(self, iq):
return None
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('result', response['type'])
def test_successPayload(self):
"""
Test response when the request is handled successfully with payload.
"""
class Handler(DummyIQHandler):
payload = domish.Element(('testns', 'foo'))
def onGet(self, iq):
return self.payload
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('result', response['type'])
payload = response.elements().next()
self.assertEqual(handler.payload, payload)
def test_successDeferred(self):
"""
Test response when where the handler was a deferred.
"""
class Handler(DummyIQHandler):
def onGet(self, iq):
return defer.succeed(None)
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('result', response['type'])
def test_failure(self):
"""
Test response when the request is handled unsuccessfully.
"""
class Handler(DummyIQHandler):
def onGet(self, iq):
raise error.StanzaError('forbidden')
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('error', response['type'])
e = error.exceptionFromStanza(response)
self.assertEquals('forbidden', e.condition)
def test_failureUnknown(self):
"""
Test response when the request handler raises a non-stanza-error.
"""
class TestError(Exception):
pass
class Handler(DummyIQHandler):
def onGet(self, iq):
raise TestError()
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('error', response['type'])
e = error.exceptionFromStanza(response)
self.assertEquals('internal-server-error', e.condition)
self.assertEquals(1, len(self.flushLoggedErrors(TestError)))
def test_notImplemented(self):
"""
Test response when the request is recognised but not implemented.
"""
class Handler(DummyIQHandler):
def onGet(self, iq):
raise NotImplementedError()
iq = domish.Element((None, 'iq'))
iq['type'] = 'get'
iq['id'] = 'r1'
handler = Handler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('error', response['type'])
e = error.exceptionFromStanza(response)
self.assertEquals('feature-not-implemented', e.condition)
def test_noHandler(self):
"""
Test when the request is not recognised.
"""
iq = domish.Element((None, 'iq'))
iq['type'] = 'set'
iq['id'] = 'r1'
handler = DummyIQHandler()
handler.handleRequest(iq)
response = handler.output[-1]
self.assertEquals(None, response.uri)
self.assertEquals('iq', response.name)
self.assertEquals('error', response['type'])
e = error.exceptionFromStanza(response)
self.assertEquals('feature-not-implemented', e.condition)
wokkel-0.7.1/wokkel/test/test_ping.py 0000775 0001750 0001750 00000013341 11707215356 020442 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.ping}.
"""
from zope.interface import verify
from twisted.internet import defer
from twisted.trial import unittest
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import toResponse
from wokkel import disco, iwokkel, ping
from wokkel.generic import parseXml
from wokkel.test.helpers import XmlStreamStub
class PingClientProtocolTest(unittest.TestCase):
"""
Tests for L{ping.PingClientProtocol}.
"""
def setUp(self):
self.stub = XmlStreamStub()
self.protocol = ping.PingClientProtocol()
self.protocol.xmlstream = self.stub.xmlstream
self.protocol.connectionInitialized()
def test_ping(self):
"""
Pinging a service should fire a deferred with None
"""
def cb(result):
self.assertIdentical(None, result)
d = self.protocol.ping(JID("example.com"))
d.addCallback(cb)
iq = self.stub.output[-1]
self.assertEqual(u'example.com', iq.getAttribute(u'to'))
self.assertEqual(u'get', iq.getAttribute(u'type'))
self.assertEqual('urn:xmpp:ping', iq.ping.uri)
response = toResponse(iq, u'result')
self.stub.send(response)
return d
def test_pingWithSender(self):
"""
Pinging a service with a sender address should include that address.
"""
d = self.protocol.ping(JID("example.com"),
sender=JID('user@example.com'))
iq = self.stub.output[-1]
self.assertEqual(u'user@example.com', iq.getAttribute(u'from'))
response = toResponse(iq, u'result')
self.stub.send(response)
return d
def test_pingNotSupported(self):
"""
Pinging a service should fire a deferred with None if not supported.
"""
def cb(result):
self.assertIdentical(None, result)
d = self.protocol.ping(JID("example.com"))
d.addCallback(cb)
iq = self.stub.output[-1]
exc = StanzaError('service-unavailable')
response = exc.toResponse(iq)
self.stub.send(response)
return d
def test_pingStanzaError(self):
"""
Pinging a service should errback a deferred on other (stanza) errors.
"""
def cb(exc):
self.assertEquals('item-not-found', exc.condition)
d = self.protocol.ping(JID("example.com"))
self.assertFailure(d, StanzaError)
d.addCallback(cb)
iq = self.stub.output[-1]
exc = StanzaError('item-not-found')
response = exc.toResponse(iq)
self.stub.send(response)
return d
class PingHandlerTest(unittest.TestCase):
"""
Tests for L{ping.PingHandler}.
"""
def setUp(self):
self.stub = XmlStreamStub()
self.protocol = ping.PingHandler()
self.protocol.xmlstream = self.stub.xmlstream
self.protocol.connectionInitialized()
def test_onPing(self):
"""
A ping should have a simple result response.
"""
xml = """
"""
self.stub.send(parseXml(xml))
response = self.stub.output[-1]
self.assertEquals('example.com', response.getAttribute('from'))
self.assertEquals('test@example.com', response.getAttribute('to'))
self.assertEquals('result', response.getAttribute('type'))
def test_onPingHandled(self):
"""
The ping handler should mark the stanza as handled.
"""
xml = """
"""
iq = parseXml(xml)
self.stub.send(iq)
self.assertTrue(iq.handled)
def test_interfaceIDisco(self):
"""
The ping handler should provice Service Discovery information.
"""
verify.verifyObject(iwokkel.IDisco, self.protocol)
def test_getDiscoInfo(self):
"""
The ping namespace should be returned as a supported feature.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertIn('urn:xmpp:ping', discoInfo.features)
d = defer.maybeDeferred(self.protocol.getDiscoInfo,
JID('user@example.org/home'),
JID('pubsub.example.org'),
'')
d.addCallback(cb)
return d
def test_getDiscoInfoNode(self):
"""
The ping namespace should not be returned for a node.
"""
def cb(info):
discoInfo = disco.DiscoInfo()
for item in info:
discoInfo.append(item)
self.assertNotIn('urn:xmpp:ping', discoInfo.features)
d = defer.maybeDeferred(self.protocol.getDiscoInfo,
JID('user@example.org/home'),
JID('pubsub.example.org'),
'test')
d.addCallback(cb)
return d
def test_getDiscoItems(self):
"""
Items are not supported by this handler, so an empty list is expected.
"""
def cb(items):
self.assertEquals(0, len(items))
d = defer.maybeDeferred(self.protocol.getDiscoItems,
JID('user@example.org/home'),
JID('pubsub.example.org'),
'')
d.addCallback(cb)
return d
wokkel-0.7.1/wokkel/test/test_generic.py 0000775 0001750 0001750 00000022662 12074265316 021126 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Tests for L{wokkel.generic}.
"""
from twisted.trial import unittest
from twisted.words.xish import domish
from twisted.words.protocols.jabber.jid import JID
from wokkel import generic
from wokkel.test.helpers import XmlStreamStub
NS_VERSION = 'jabber:iq:version'
class VersionHandlerTest(unittest.TestCase):
"""
Tests for L{wokkel.generic.VersionHandler}.
"""
def test_onVersion(self):
"""
Test response to incoming version request.
"""
self.stub = XmlStreamStub()
self.protocol = generic.VersionHandler('Test', '0.1.0')
self.protocol.xmlstream = self.stub.xmlstream
self.protocol.send = self.stub.xmlstream.send
self.protocol.connectionInitialized()
iq = domish.Element((None, 'iq'))
iq['from'] = 'user@example.org/Home'
iq['to'] = 'example.org'
iq['type'] = 'get'
iq.addElement((NS_VERSION, 'query'))
self.stub.send(iq)
response = self.stub.output[-1]
self.assertEquals('user@example.org/Home', response['to'])
self.assertEquals('example.org', response['from'])
self.assertEquals('result', response['type'])
self.assertEquals(NS_VERSION, response.query.uri)
elements = list(domish.generateElementsQNamed(response.query.children,
'name', NS_VERSION))
self.assertEquals(1, len(elements))
self.assertEquals('Test', unicode(elements[0]))
elements = list(domish.generateElementsQNamed(response.query.children,
'version', NS_VERSION))
self.assertEquals(1, len(elements))
self.assertEquals('0.1.0', unicode(elements[0]))
class XmlPipeTest(unittest.TestCase):
"""
Tests for L{wokkel.generic.XmlPipe}.
"""
def setUp(self):
self.pipe = generic.XmlPipe()
def test_sendFromSource(self):
"""
Send an element from the source and observe it from the sink.
"""
def cb(obj):
called.append(obj)
called = []
self.pipe.sink.addObserver('/test[@xmlns="testns"]', cb)
element = domish.Element(('testns', 'test'))
self.pipe.source.send(element)
self.assertEquals([element], called)
def test_sendFromSink(self):
"""
Send an element from the sink and observe it from the source.
"""
def cb(obj):
called.append(obj)
called = []
self.pipe.source.addObserver('/test[@xmlns="testns"]', cb)
element = domish.Element(('testns', 'test'))
self.pipe.sink.send(element)
self.assertEquals([element], called)
class StanzaTest(unittest.TestCase):
"""
Tests for L{generic.Stanza}.
"""
def test_fromElement(self):
xml = """
"""
stanza = generic.Stanza.fromElement(generic.parseXml(xml))
self.assertEqual('chat', stanza.stanzaType)
self.assertEqual(JID('other@example.org'), stanza.sender)
self.assertEqual(JID('user@example.org'), stanza.recipient)
def test_fromElementChildParser(self):
"""
Child elements for which no parser is defined are ignored.
"""
xml = """
"""
class Message(generic.Stanza):
childParsers = {('http://example.org/', 'x'): '_childParser_x'}
elements = []
def _childParser_x(self, element):
self.elements.append(element)
message = Message.fromElement(generic.parseXml(xml))
self.assertEqual(1, len(message.elements))
def test_fromElementChildParserAll(self):
"""
Child elements for which no parser is defined are ignored.
"""
xml = """
"""
class Message(generic.Stanza):
childParsers = {None: '_childParser'}
elements = []
def _childParser(self, element):
self.elements.append(element)
message = Message.fromElement(generic.parseXml(xml))
self.assertEqual(1, len(message.elements))
def test_fromElementChildParserUnknown(self):
"""
Child elements for which no parser is defined are ignored.
"""
xml = """
"""
generic.Stanza.fromElement(generic.parseXml(xml))
class RequestTest(unittest.TestCase):
"""
Tests for L{generic.Request}.
"""
def setUp(self):
self.request = generic.Request()
def test_requestParser(self):
"""
The request's child element is passed to requestParser.
"""
xml = """
"""
class VersionRequest(generic.Request):
elements = []
def parseRequest(self, element):
self.elements.append((element.uri, element.name))
request = VersionRequest.fromElement(generic.parseXml(xml))
self.assertEqual([(NS_VERSION, 'query')], request.elements)
def test_toElementStanzaKind(self):
"""
A request is an iq stanza.
"""
element = self.request.toElement()
self.assertIdentical(None, element.uri)
self.assertEquals('iq', element.name)
def test_toElementStanzaType(self):
"""
The request has type 'get'.
"""
self.assertEquals('get', self.request.stanzaType)
element = self.request.toElement()
self.assertEquals('get', element.getAttribute('type'))
def test_toElementStanzaTypeSet(self):
"""
The request has type 'set'.
"""
self.request.stanzaType = 'set'
element = self.request.toElement()
self.assertEquals('set', element.getAttribute('type'))
def test_toElementStanzaID(self):
"""
A request, when rendered, has an identifier.
"""
element = self.request.toElement()
self.assertNotIdentical(None, self.request.stanzaID)
self.assertEquals(self.request.stanzaID, element.getAttribute('id'))
def test_toElementRecipient(self):
"""
A request without recipient, has no 'to' attribute.
"""
self.request = generic.Request(recipient=JID('other@example.org'))
self.assertEquals(JID('other@example.org'), self.request.recipient)
element = self.request.toElement()
self.assertEquals(u'other@example.org', element.getAttribute('to'))
def test_toElementRecipientNone(self):
"""
A request without recipient, has no 'to' attribute.
"""
element = self.request.toElement()
self.assertFalse(element.hasAttribute('to'))
def test_toElementSender(self):
"""
A request with sender, has a 'from' attribute.
"""
self.request = generic.Request(sender=JID('user@example.org'))
self.assertEquals(JID('user@example.org'), self.request.sender)
element = self.request.toElement()
self.assertEquals(u'user@example.org', element.getAttribute('from'))
def test_toElementSenderNone(self):
"""
A request without sender, has no 'from' attribute.
"""
element = self.request.toElement()
self.assertFalse(element.hasAttribute('from'))
def test_timeoutDefault(self):
"""
The default is no timeout.
"""
self.assertIdentical(None, self.request.timeout)
class PrepareIDNNameTests(unittest.TestCase):
"""
Tests for L{wokkel.generic.prepareIDNName}.
"""
def test_bytestring(self):
"""
An ASCII-encoded byte string is left as-is.
"""
name = b"example.com"
result = generic.prepareIDNName(name)
self.assertEqual(b"example.com", result)
def test_unicode(self):
"""
A unicode all-ASCII name is converted to an ASCII byte string.
"""
name = u"example.com"
result = generic.prepareIDNName(name)
self.assertEqual(b"example.com", result)
def test_unicodeNonASCII(self):
"""
A unicode with non-ASCII is converted to its ACE equivalent.
"""
name = u"\u00e9chec.example.com"
result = generic.prepareIDNName(name)
self.assertEqual(b"xn--chec-9oa.example.com", result)
def test_unicodeHalfwidthIdeographicFullStop(self):
"""
Exotic dots in unicode names are converted to Full Stop.
"""
name = u"\u00e9chec.example\uff61com"
result = generic.prepareIDNName(name)
self.assertEqual(b"xn--chec-9oa.example.com", result)
def test_unicodeTrailingDot(self):
"""
Unicode names with trailing dots retain the trailing dot.
L{encodings.idna.ToASCII} doesn't allow the empty string as the input,
hence the implementation needs to strip a trailing dot, and re-add it
after encoding the labels.
"""
name = u"example.com."
result = generic.prepareIDNName(name)
self.assertEqual(b"example.com.", result)
wokkel-0.7.1/wokkel/__init__.py 0000664 0001750 0001750 00000000211 12040670215 017201 0 ustar ralphm ralphm 0000000 0000000 # Copyright (c) Ralph Meijer.
# See LICENSE for details
"""
Wokkel.
Support library for Twisted applications using XMPP protocols.
"""
wokkel-0.7.1/wokkel/pubsub.py 0000775 0001750 0001750 00000143325 12014253401 016756 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_pubsub -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP publish-subscribe protocol.
This protocol is specified in
U{XEP-0060}.
"""
from zope.interface import implements
from twisted.internet import defer
from twisted.python import log
from twisted.words.protocols.jabber import jid, error
from twisted.words.xish import domish
from wokkel import disco, data_form, generic, shim
from wokkel.compat import IQ
from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource
# Iq get and set XPath queries
IQ_GET = '/iq[@type="get"]'
IQ_SET = '/iq[@type="set"]'
# Publish-subscribe namespaces
NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
NS_PUBSUB_EVENT = NS_PUBSUB + '#event'
NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors'
NS_PUBSUB_OWNER = NS_PUBSUB + "#owner"
NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options"
# XPath to match pubsub requests
PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \
'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
'@xmlns="' + NS_PUBSUB_OWNER + '"]'
class SubscriptionPending(Exception):
"""
Raised when the requested subscription is pending acceptance.
"""
class SubscriptionUnconfigured(Exception):
"""
Raised when the requested subscription needs to be configured before
becoming active.
"""
class PubSubError(error.StanzaError):
"""
Exception with publish-subscribe specific condition.
"""
def __init__(self, condition, pubsubCondition, feature=None, text=None):
appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
if feature:
appCondition['feature'] = feature
error.StanzaError.__init__(self, condition,
text=text,
appCondition=appCondition)
class BadRequest(error.StanzaError):
"""
Bad request stanza error.
"""
def __init__(self, pubsubCondition=None, text=None):
if pubsubCondition:
appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
else:
appCondition = None
error.StanzaError.__init__(self, 'bad-request',
text=text,
appCondition=appCondition)
class Unsupported(PubSubError):
def __init__(self, feature, text=None):
self.feature = feature
PubSubError.__init__(self, 'feature-not-implemented',
'unsupported',
feature,
text)
def __str__(self):
message = PubSubError.__str__(self)
message += ', feature %r' % self.feature
return message
class Subscription(object):
"""
A subscription to a node.
@ivar nodeIdentifier: The identifier of the node subscribed to. The root
node is denoted by C{None}.
@type nodeIdentifier: C{unicode}
@ivar subscriber: The subscribing entity.
@type subscriber: L{jid.JID}
@ivar state: The subscription state. One of C{'subscribed'}, C{'pending'},
C{'unconfigured'}.
@type state: C{unicode}
@ivar options: Optional list of subscription options.
@type options: C{dict}
@ivar subscriptionIdentifier: Optional subscription identifier.
@type subscriptionIdentifier: C{unicode}
"""
def __init__(self, nodeIdentifier, subscriber, state, options=None,
subscriptionIdentifier=None):
self.nodeIdentifier = nodeIdentifier
self.subscriber = subscriber
self.state = state
self.options = options or {}
self.subscriptionIdentifier = subscriptionIdentifier
@staticmethod
def fromElement(element):
return Subscription(
element.getAttribute('node'),
jid.JID(element.getAttribute('jid')),
element.getAttribute('subscription'),
subscriptionIdentifier=element.getAttribute('subid'))
def toElement(self, defaultUri=None):
"""
Return the DOM representation of this subscription.
@rtype: L{domish.Element}
"""
element = domish.Element((defaultUri, 'subscription'))
if self.nodeIdentifier:
element['node'] = self.nodeIdentifier
element['jid'] = unicode(self.subscriber)
element['subscription'] = self.state
if self.subscriptionIdentifier:
element['subid'] = self.subscriptionIdentifier
return element
class Item(domish.Element):
"""
Publish subscribe item.
This behaves like an object providing L{domish.IElement}.
Item payload can be added using C{addChild} or C{addRawXml}, or using the
C{payload} keyword argument to C{__init__}.
"""
def __init__(self, id=None, payload=None):
"""
@param id: optional item identifier
@type id: C{unicode}
@param payload: optional item payload. Either as a domish element, or
as serialized XML.
@type payload: object providing L{domish.IElement} or C{unicode}.
"""
domish.Element.__init__(self, (None, 'item'))
if id is not None:
self['id'] = id
if payload is not None:
if isinstance(payload, basestring):
self.addRawXml(payload)
else:
self.addChild(payload)
class PubSubRequest(generic.Stanza):
"""
A publish-subscribe request.
The set of instance variables used depends on the type of request. If
a variable is not applicable or not passed in the request, its value is
C{None}.
@ivar verb: The type of publish-subscribe request. See C{_requestVerbMap}.
@type verb: C{str}.
@ivar affiliations: Affiliations to be modified.
@type affiliations: C{set}
@ivar items: The items to be published, as L{domish.Element}s.
@type items: C{list}
@ivar itemIdentifiers: Identifiers of the items to be retrieved or
retracted.
@type itemIdentifiers: C{set}
@ivar maxItems: Maximum number of items to retrieve.
@type maxItems: C{int}.
@ivar nodeIdentifier: Identifier of the node the request is about.
@type nodeIdentifier: C{unicode}
@ivar nodeType: The type of node that should be created, or for which the
configuration is retrieved. C{'leaf'} or C{'collection'}.
@type nodeType: C{str}
@ivar options: Configurations options for nodes, subscriptions and publish
requests.
@type options: L{data_form.Form}
@ivar subscriber: The subscribing entity.
@type subscriber: L{JID}
@ivar subscriptionIdentifier: Identifier for a specific subscription.
@type subscriptionIdentifier: C{unicode}
@ivar subscriptions: Subscriptions to be modified, as a set of
L{Subscription}.
@type subscriptions: C{set}
@ivar affiliations: Affiliations to be modified, as a dictionary of entity
(L{JID} to affiliation
(C{unicode}).
@type affiliations: C{dict}
"""
verb = None
affiliations = None
items = None
itemIdentifiers = None
maxItems = None
nodeIdentifier = None
nodeType = None
options = None
subscriber = None
subscriptionIdentifier = None
subscriptions = None
affiliations = None
# Map request iq type and subelement name to request verb
_requestVerbMap = {
('set', NS_PUBSUB, 'publish'): 'publish',
('set', NS_PUBSUB, 'subscribe'): 'subscribe',
('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe',
('get', NS_PUBSUB, 'options'): 'optionsGet',
('set', NS_PUBSUB, 'options'): 'optionsSet',
('get', NS_PUBSUB, 'subscriptions'): 'subscriptions',
('get', NS_PUBSUB, 'affiliations'): 'affiliations',
('set', NS_PUBSUB, 'create'): 'create',
('get', NS_PUBSUB_OWNER, 'default'): 'default',
('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet',
('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet',
('get', NS_PUBSUB, 'items'): 'items',
('set', NS_PUBSUB, 'retract'): 'retract',
('set', NS_PUBSUB_OWNER, 'purge'): 'purge',
('set', NS_PUBSUB_OWNER, 'delete'): 'delete',
('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet',
('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet',
('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet',
('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet',
}
# Map request verb to request iq type and subelement name
_verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
# Map request verb to parameter handler names
_parameters = {
'publish': ['node', 'items'],
'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'],
'unsubscribe': ['nodeOrEmpty', 'jid', 'subidOrNone'],
'optionsGet': ['nodeOrEmpty', 'jid', 'subidOrNone'],
'optionsSet': ['nodeOrEmpty', 'jid', 'options', 'subidOrNone'],
'subscriptions': [],
'affiliations': [],
'create': ['nodeOrNone', 'configureOrNone'],
'default': ['default'],
'configureGet': ['nodeOrEmpty'],
'configureSet': ['nodeOrEmpty', 'configure'],
'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'],
'retract': ['node', 'itemIdentifiers'],
'purge': ['node'],
'delete': ['node'],
'affiliationsGet': ['nodeOrEmpty'],
'affiliationsSet': ['nodeOrEmpty', 'affiliations'],
'subscriptionsGet': ['nodeOrEmpty'],
'subscriptionsSet': [],
}
def __init__(self, verb=None):
self.verb = verb
def _parse_node(self, verbElement):
"""
Parse the required node identifier out of the verbElement.
"""
try:
self.nodeIdentifier = verbElement["node"]
except KeyError:
raise BadRequest('nodeid-required')
def _render_node(self, verbElement):
"""
Render the required node identifier on the verbElement.
"""
if not self.nodeIdentifier:
raise Exception("Node identifier is required")
verbElement['node'] = self.nodeIdentifier
def _parse_nodeOrEmpty(self, verbElement):
"""
Parse the node identifier out of the verbElement. May be empty.
"""
self.nodeIdentifier = verbElement.getAttribute("node", '')
def _render_nodeOrEmpty(self, verbElement):
"""
Render the node identifier on the verbElement. May be empty.
"""
if self.nodeIdentifier:
verbElement['node'] = self.nodeIdentifier
def _parse_nodeOrNone(self, verbElement):
"""
Parse the optional node identifier out of the verbElement.
"""
self.nodeIdentifier = verbElement.getAttribute("node")
def _render_nodeOrNone(self, verbElement):
"""
Render the optional node identifier on the verbElement.
"""
if self.nodeIdentifier:
verbElement['node'] = self.nodeIdentifier
def _parse_items(self, verbElement):
"""
Parse items out of the verbElement for publish requests.
"""
self.items = []
for element in verbElement.elements():
if element.uri == NS_PUBSUB and element.name == 'item':
self.items.append(element)
def _render_items(self, verbElement):
"""
Render items into the verbElement for publish requests.
"""
if self.items:
for item in self.items:
item.uri = NS_PUBSUB
verbElement.addChild(item)
def _parse_jid(self, verbElement):
"""
Parse subscriber out of the verbElement for un-/subscribe requests.
"""
try:
self.subscriber = jid.internJID(verbElement["jid"])
except KeyError:
raise BadRequest('jid-required')
def _render_jid(self, verbElement):
"""
Render subscriber into the verbElement for un-/subscribe requests.
"""
verbElement['jid'] = self.subscriber.full()
def _parse_default(self, verbElement):
"""
Parse node type out of a request for the default node configuration.
"""
form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
if form is not None and form.formType == 'submit':
values = form.getValues()
self.nodeType = values.get('pubsub#node_type', 'leaf')
else:
self.nodeType = 'leaf'
def _parse_configure(self, verbElement):
"""
Parse options out of a request for setting the node configuration.
"""
form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
if form is not None:
if form.formType in ('submit', 'cancel'):
self.options = form
else:
raise BadRequest(text=u"Unexpected form type '%s'" % form.formType)
else:
raise BadRequest(text="Missing configuration form")
def _parse_configureOrNone(self, verbElement):
"""
Parse optional node configuration form in create request.
"""
for element in verbElement.parent.elements():
if element.uri == NS_PUBSUB and element.name == 'configure':
form = data_form.findForm(element, NS_PUBSUB_NODE_CONFIG)
if form is not None:
if form.formType != 'submit':
raise BadRequest(text=u"Unexpected form type '%s'" %
form.formType)
else:
form = data_form.Form('submit',
formNamespace=NS_PUBSUB_NODE_CONFIG)
self.options = form
def _render_configureOrNone(self, verbElement):
"""
Render optional node configuration form in create request.
"""
if self.options is not None:
configure = verbElement.parent.addElement('configure')
configure.addChild(self.options.toElement())
def _parse_itemIdentifiers(self, verbElement):
"""
Parse item identifiers out of items and retract requests.
"""
self.itemIdentifiers = []
for element in verbElement.elements():
if element.uri == NS_PUBSUB and element.name == 'item':
try:
self.itemIdentifiers.append(element["id"])
except KeyError:
raise BadRequest()
def _render_itemIdentifiers(self, verbElement):
"""
Render item identifiers into items and retract requests.
"""
if self.itemIdentifiers:
for itemIdentifier in self.itemIdentifiers:
item = verbElement.addElement('item')
item['id'] = itemIdentifier
def _parse_maxItems(self, verbElement):
"""
Parse maximum items out of an items request.
"""
value = verbElement.getAttribute('max_items')
if value:
try:
self.maxItems = int(value)
except ValueError:
raise BadRequest(text="Field max_items requires a positive " +
"integer value")
def _render_maxItems(self, verbElement):
"""
Render maximum items into an items request.
"""
if self.maxItems:
verbElement['max_items'] = unicode(self.maxItems)
def _parse_subidOrNone(self, verbElement):
"""
Parse subscription identifier out of a request.
"""
self.subscriptionIdentifier = verbElement.getAttribute("subid")
def _render_subidOrNone(self, verbElement):
"""
Render subscription identifier into a request.
"""
if self.subscriptionIdentifier:
verbElement['subid'] = self.subscriptionIdentifier
def _parse_options(self, verbElement):
"""
Parse options form out of a subscription options request.
"""
form = data_form.findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS)
if form is not None:
if form.formType in ('submit', 'cancel'):
self.options = form
else:
raise BadRequest(text=u"Unexpected form type '%s'" % form.formType)
else:
raise BadRequest(text="Missing options form")
def _render_options(self, verbElement):
verbElement.addChild(self.options.toElement())
def _parse_optionsWithSubscribe(self, verbElement):
for element in verbElement.parent.elements():
if element.name == 'options' and element.uri == NS_PUBSUB:
form = data_form.findForm(element,
NS_PUBSUB_SUBSCRIBE_OPTIONS)
if form is not None:
if form.formType != 'submit':
raise BadRequest(text=u"Unexpected form type '%s'" %
form.formType)
else:
form = data_form.Form('submit',
formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
self.options = form
def _render_optionsWithSubscribe(self, verbElement):
if self.options is not None:
optionsElement = verbElement.parent.addElement('options')
self._render_options(optionsElement)
def _parse_affiliations(self, verbElement):
self.affiliations = {}
for element in verbElement.elements():
if (element.uri == NS_PUBSUB_OWNER and
element.name == 'affiliation'):
try:
entity = jid.internJID(element['jid']).userhostJID()
except KeyError:
raise BadRequest(text='Missing jid attribute')
if entity in self.affiliations:
raise BadRequest(text='Multiple affiliations for an entity')
try:
affiliation = element['affiliation']
except KeyError:
raise BadRequest(text='Missing affiliation attribute')
self.affiliations[entity] = affiliation
def parseElement(self, element):
"""
Parse the publish-subscribe verb and parameters out of a request.
"""
generic.Stanza.parseElement(self, element)
verbs = []
verbElements = []
for child in element.pubsub.elements():
key = (self.stanzaType, child.uri, child.name)
try:
verb = self._requestVerbMap[key]
except KeyError:
continue
verbs.append(verb)
verbElements.append(child)
if not verbs:
raise NotImplementedError()
if len(verbs) > 1:
if 'optionsSet' in verbs and 'subscribe' in verbs:
self.verb = 'subscribe'
verbElement = verbElements[verbs.index('subscribe')]
else:
raise NotImplementedError()
else:
self.verb = verbs[0]
verbElement = verbElements[0]
for parameter in self._parameters[self.verb]:
getattr(self, '_parse_%s' % parameter)(verbElement)
def send(self, xs):
"""
Send this request to its recipient.
This renders all of the relevant parameters for this specific
requests into an L{IQ}, and invoke its C{send} method.
This returns a deferred that fires upon reception of a response. See
L{IQ} for details.
@param xs: The XML stream to send the request on.
@type xs: L{twisted.words.protocols.jabber.xmlstream.XmlStream}
@rtype: L{defer.Deferred}.
"""
try:
(self.stanzaType,
childURI,
childName) = self._verbRequestMap[self.verb]
except KeyError:
raise NotImplementedError()
iq = IQ(xs, self.stanzaType)
iq.addElement((childURI, 'pubsub'))
verbElement = iq.pubsub.addElement(childName)
if self.sender:
iq['from'] = self.sender.full()
if self.recipient:
iq['to'] = self.recipient.full()
for parameter in self._parameters[self.verb]:
getattr(self, '_render_%s' % parameter)(verbElement)
return iq.send()
class PubSubEvent(object):
"""
A publish subscribe event.
@param sender: The entity from which the notification was received.
@type sender: L{jid.JID}
@param recipient: The entity to which the notification was sent.
@type recipient: L{wokkel.pubsub.ItemsEvent}
@param nodeIdentifier: Identifier of the node the event pertains to.
@type nodeIdentifier: C{unicode}
@param headers: SHIM headers, see L{wokkel.shim.extractHeaders}.
@type headers: C{dict}
"""
def __init__(self, sender, recipient, nodeIdentifier, headers):
self.sender = sender
self.recipient = recipient
self.nodeIdentifier = nodeIdentifier
self.headers = headers
class ItemsEvent(PubSubEvent):
"""
A publish-subscribe event that signifies new, updated and retracted items.
@param items: List of received items as domish elements.
@type items: C{list} of L{domish.Element}
"""
def __init__(self, sender, recipient, nodeIdentifier, items, headers):
PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers)
self.items = items
class DeleteEvent(PubSubEvent):
"""
A publish-subscribe event that signifies the deletion of a node.
"""
redirectURI = None
class PurgeEvent(PubSubEvent):
"""
A publish-subscribe event that signifies the purging of a node.
"""
class PubSubClient(XMPPHandler):
"""
Publish subscribe client protocol.
"""
implements(IPubSubClient)
def connectionInitialized(self):
self.xmlstream.addObserver('/message/event[@xmlns="%s"]' %
NS_PUBSUB_EVENT, self._onEvent)
def _onEvent(self, message):
if message.getAttribute('type') == 'error':
return
try:
sender = jid.JID(message["from"])
recipient = jid.JID(message["to"])
except KeyError:
return
actionElement = None
for element in message.event.elements():
if element.uri == NS_PUBSUB_EVENT:
actionElement = element
if not actionElement:
return
eventHandler = getattr(self, "_onEvent_%s" % actionElement.name, None)
if eventHandler:
headers = shim.extractHeaders(message)
eventHandler(sender, recipient, actionElement, headers)
message.handled = True
def _onEvent_items(self, sender, recipient, action, headers):
nodeIdentifier = action["node"]
items = [element for element in action.elements()
if element.name in ('item', 'retract')]
event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers)
self.itemsReceived(event)
def _onEvent_delete(self, sender, recipient, action, headers):
nodeIdentifier = action["node"]
event = DeleteEvent(sender, recipient, nodeIdentifier, headers)
if action.redirect:
event.redirectURI = action.redirect.getAttribute('uri')
self.deleteReceived(event)
def _onEvent_purge(self, sender, recipient, action, headers):
nodeIdentifier = action["node"]
event = PurgeEvent(sender, recipient, nodeIdentifier, headers)
self.purgeReceived(event)
def itemsReceived(self, event):
pass
def deleteReceived(self, event):
pass
def purgeReceived(self, event):
pass
def createNode(self, service, nodeIdentifier=None, options=None,
sender=None):
"""
Create a publish subscribe node.
@param service: The publish subscribe service to create the node at.
@type service: L{JID}
@param nodeIdentifier: Optional suggestion for the id of the node.
@type nodeIdentifier: C{unicode}
@param options: Optional node configuration options.
@type options: C{dict}
"""
request = PubSubRequest('create')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.sender = sender
if options:
form = data_form.Form(formType='submit',
formNamespace=NS_PUBSUB_NODE_CONFIG)
form.makeFields(options)
request.options = form
def cb(iq):
try:
new_node = iq.pubsub.create["node"]
except AttributeError:
# the suggested node identifier was accepted
new_node = nodeIdentifier
return new_node
d = request.send(self.xmlstream)
d.addCallback(cb)
return d
def deleteNode(self, service, nodeIdentifier, sender=None):
"""
Delete a publish subscribe node.
@param service: The publish subscribe service to delete the node from.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
"""
request = PubSubRequest('delete')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.sender = sender
return request.send(self.xmlstream)
def subscribe(self, service, nodeIdentifier, subscriber,
options=None, sender=None):
"""
Subscribe to a publish subscribe node.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity to subscribe to the node. This entity
will get notifications of new published items.
@type subscriber: L{JID}
@param options: Subscription options.
@type options: C{dict}
@return: Deferred that fires with L{Subscription} or errbacks with
L{SubscriptionPending} or L{SubscriptionUnconfigured}.
@rtype: L{defer.Deferred}
"""
request = PubSubRequest('subscribe')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.subscriber = subscriber
request.sender = sender
if options:
form = data_form.Form(formType='submit',
formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
form.makeFields(options)
request.options = form
def cb(iq):
subscription = Subscription.fromElement(iq.pubsub.subscription)
if subscription.state == 'pending':
raise SubscriptionPending()
elif subscription.state == 'unconfigured':
raise SubscriptionUnconfigured()
else:
# we assume subscription == 'subscribed'
# any other value would be invalid, but that should have
# yielded a stanza error.
return subscription
d = request.send(self.xmlstream)
d.addCallback(cb)
return d
def unsubscribe(self, service, nodeIdentifier, subscriber,
subscriptionIdentifier=None, sender=None):
"""
Unsubscribe from a publish subscribe node.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity to unsubscribe from the node.
@type subscriber: L{JID}
@param subscriptionIdentifier: Optional subscription identifier.
@type subscriptionIdentifier: C{unicode}
"""
request = PubSubRequest('unsubscribe')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.subscriber = subscriber
request.subscriptionIdentifier = subscriptionIdentifier
request.sender = sender
return request.send(self.xmlstream)
def publish(self, service, nodeIdentifier, items=None, sender=None):
"""
Publish to a publish subscribe node.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param items: Optional list of L{Item}s to publish.
@type items: C{list}
"""
request = PubSubRequest('publish')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.items = items
request.sender = sender
return request.send(self.xmlstream)
def items(self, service, nodeIdentifier, maxItems=None,
subscriptionIdentifier=None, sender=None):
"""
Retrieve previously published items from a publish subscribe node.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param maxItems: Optional limit on the number of retrieved items.
@type maxItems: C{int}
@param subscriptionIdentifier: Optional subscription identifier. In
case the node has been subscribed to multiple times, this narrows
the results to the specific subscription.
@type subscriptionIdentifier: C{unicode}
"""
request = PubSubRequest('items')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
if maxItems:
request.maxItems = str(int(maxItems))
request.subscriptionIdentifier = subscriptionIdentifier
request.sender = sender
def cb(iq):
items = []
for element in iq.pubsub.items.elements():
if element.uri == NS_PUBSUB and element.name == 'item':
items.append(element)
return items
d = request.send(self.xmlstream)
d.addCallback(cb)
return d
def getOptions(self, service, nodeIdentifier, subscriber,
subscriptionIdentifier=None, sender=None):
"""
Get subscription options.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity subscribed to the node.
@type subscriber: L{JID}
@param subscriptionIdentifier: Optional subscription identifier.
@type subscriptionIdentifier: C{unicode}
@rtype: L{data_form.Form}
"""
request = PubSubRequest('optionsGet')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.subscriber = subscriber
request.subscriptionIdentifier = subscriptionIdentifier
request.sender = sender
def cb(iq):
form = data_form.findForm(iq.pubsub.options,
NS_PUBSUB_SUBSCRIBE_OPTIONS)
form.typeCheck()
return form
d = request.send(self.xmlstream)
d.addCallback(cb)
return d
def setOptions(self, service, nodeIdentifier, subscriber,
options, subscriptionIdentifier=None, sender=None):
"""
Set subscription options.
@param service: The publish subscribe service that keeps the node.
@type service: L{JID}
@param nodeIdentifier: The identifier of the node.
@type nodeIdentifier: C{unicode}
@param subscriber: The entity subscribed to the node.
@type subscriber: L{JID}
@param options: Subscription options.
@type options: C{dict}.
@param subscriptionIdentifier: Optional subscription identifier.
@type subscriptionIdentifier: C{unicode}
"""
request = PubSubRequest('optionsSet')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.subscriber = subscriber
request.subscriptionIdentifier = subscriptionIdentifier
request.sender = sender
form = data_form.Form(formType='submit',
formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
form.makeFields(options)
request.options = form
d = request.send(self.xmlstream)
return d
class PubSubService(XMPPHandler, IQHandlerMixin):
"""
Protocol implementation for a XMPP Publish Subscribe Service.
The word Service here is used as taken from the Publish Subscribe
specification. It is the party responsible for keeping nodes and their
subscriptions, and sending out notifications.
Methods from the L{IPubSubService} interface that are called as a result
of an XMPP request may raise exceptions. Alternatively the deferred
returned by these methods may have their errback called. These are handled
as follows:
- If the exception is an instance of L{error.StanzaError}, an error
response iq is returned.
- Any other exception is reported using L{log.msg}. An error response
with the condition C{internal-server-error} is returned.
The default implementation of said methods raises an L{Unsupported}
exception and are meant to be overridden.
@ivar discoIdentity: Service discovery identity as a dictionary with
keys C{'category'}, C{'type'} and C{'name'}.
@ivar pubSubFeatures: List of supported publish-subscribe features for
service discovery, as C{str}.
@type pubSubFeatures: C{list} or C{None}
"""
implements(IPubSubService, disco.IDisco)
iqHandlers = {
'/*': '_onPubSubRequest',
}
_legacyHandlers = {
'publish': ('publish', ['sender', 'recipient',
'nodeIdentifier', 'items']),
'subscribe': ('subscribe', ['sender', 'recipient',
'nodeIdentifier', 'subscriber']),
'unsubscribe': ('unsubscribe', ['sender', 'recipient',
'nodeIdentifier', 'subscriber']),
'subscriptions': ('subscriptions', ['sender', 'recipient']),
'affiliations': ('affiliations', ['sender', 'recipient']),
'create': ('create', ['sender', 'recipient', 'nodeIdentifier']),
'getConfigurationOptions': ('getConfigurationOptions', []),
'default': ('getDefaultConfiguration',
['sender', 'recipient', 'nodeType']),
'configureGet': ('getConfiguration', ['sender', 'recipient',
'nodeIdentifier']),
'configureSet': ('setConfiguration', ['sender', 'recipient',
'nodeIdentifier', 'options']),
'items': ('items', ['sender', 'recipient', 'nodeIdentifier',
'maxItems', 'itemIdentifiers']),
'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier',
'itemIdentifiers']),
'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']),
'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']),
}
hideNodes = False
def __init__(self, resource=None):
self.resource = resource
self.discoIdentity = {'category': 'pubsub',
'type': 'service',
'name': 'Generic Publish-Subscribe Service'}
self.pubSubFeatures = []
def connectionMade(self):
self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
def toInfo(nodeInfo):
if not nodeInfo:
return
(nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
info.append(disco.DiscoIdentity('pubsub', nodeType))
if metaData:
form = data_form.Form(formType="result",
formNamespace=NS_PUBSUB_META_DATA)
form.addField(
data_form.Field(
var='pubsub#node_type',
value=nodeType,
label='The type of node (collection or leaf)'
)
)
for metaDatum in metaData:
form.addField(data_form.Field.fromDict(metaDatum))
info.append(form)
return
info = []
request = PubSubRequest('discoInfo')
if self.resource is not None:
resource = self.resource.locateResource(request)
identity = resource.discoIdentity
features = resource.features
getInfo = resource.getInfo
else:
category = self.discoIdentity['category']
idType = self.discoIdentity['type']
name = self.discoIdentity['name']
identity = disco.DiscoIdentity(category, idType, name)
features = self.pubSubFeatures
getInfo = self.getNodeInfo
if not nodeIdentifier:
info.append(identity)
info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS))
info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
for feature in features])
d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '')
d.addCallback(toInfo)
d.addErrback(log.err)
d.addCallback(lambda _: info)
return d
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
if self.hideNodes:
d = defer.succeed([])
elif self.resource is not None:
request = PubSubRequest('discoInfo')
resource = self.resource.locateResource(request)
d = resource.getNodes(requestor, target, nodeIdentifier)
elif nodeIdentifier:
d = self.getNodes(requestor, target)
else:
d = defer.succeed([])
d.addCallback(lambda nodes: [disco.DiscoItem(target, node)
for node in nodes])
return d
def _onPubSubRequest(self, iq):
request = PubSubRequest.fromElement(iq)
if self.resource is not None:
resource = self.resource.locateResource(request)
else:
resource = self
# Preprocess the request, knowing the handling resource
try:
preProcessor = getattr(self, '_preProcess_%s' % request.verb)
except AttributeError:
pass
else:
request = preProcessor(resource, request)
if request is None:
return defer.succeed(None)
# Process the request itself,
if resource is not self:
try:
handler = getattr(resource, request.verb)
except AttributeError:
text = "Request verb: %s" % request.verb
return defer.fail(Unsupported('', text))
d = handler(request)
else:
try:
handlerName, argNames = self._legacyHandlers[request.verb]
except KeyError:
text = "Request verb: %s" % request.verb
return defer.fail(Unsupported('', text))
handler = getattr(self, handlerName)
args = [getattr(request, arg) for arg in argNames]
d = handler(*args)
# If needed, translate the result into a response
try:
cb = getattr(self, '_toResponse_%s' % request.verb)
except AttributeError:
pass
else:
d.addCallback(cb, resource, request)
return d
def _toResponse_subscribe(self, result, resource, request):
response = domish.Element((NS_PUBSUB, "pubsub"))
response.addChild(result.toElement(NS_PUBSUB))
return response
def _toResponse_subscriptions(self, result, resource, request):
response = domish.Element((NS_PUBSUB, 'pubsub'))
subscriptions = response.addElement('subscriptions')
for subscription in result:
subscriptions.addChild(subscription.toElement(NS_PUBSUB))
return response
def _toResponse_affiliations(self, result, resource, request):
response = domish.Element((NS_PUBSUB, 'pubsub'))
affiliations = response.addElement('affiliations')
for nodeIdentifier, affiliation in result:
item = affiliations.addElement('affiliation')
item['node'] = nodeIdentifier
item['affiliation'] = affiliation
return response
def _toResponse_create(self, result, resource, request):
if not request.nodeIdentifier or request.nodeIdentifier != result:
response = domish.Element((NS_PUBSUB, 'pubsub'))
create = response.addElement('create')
create['node'] = result
return response
else:
return None
def _formFromConfiguration(self, resource, values):
fieldDefs = resource.getConfigurationOptions()
form = data_form.Form(formType="form",
formNamespace=NS_PUBSUB_NODE_CONFIG)
form.makeFields(values, fieldDefs)
return form
def _checkConfiguration(self, resource, form):
fieldDefs = resource.getConfigurationOptions()
form.typeCheck(fieldDefs, filterUnknown=True)
def _preProcess_create(self, resource, request):
if request.options:
self._checkConfiguration(resource, request.options)
return request
def _preProcess_default(self, resource, request):
if request.nodeType not in ('leaf', 'collection'):
raise error.StanzaError('not-acceptable')
else:
return request
def _toResponse_default(self, options, resource, request):
response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
default = response.addElement("default")
form = self._formFromConfiguration(resource, options)
default.addChild(form.toElement())
return response
def _toResponse_configureGet(self, options, resource, request):
response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
configure = response.addElement("configure")
form = self._formFromConfiguration(resource, options)
configure.addChild(form.toElement())
if request.nodeIdentifier:
configure["node"] = request.nodeIdentifier
return response
def _preProcess_configureSet(self, resource, request):
if request.options.formType == 'cancel':
return None
else:
self._checkConfiguration(resource, request.options)
return request
def _toResponse_items(self, result, resource, request):
response = domish.Element((NS_PUBSUB, 'pubsub'))
items = response.addElement('items')
items["node"] = request.nodeIdentifier
for item in result:
item.uri = NS_PUBSUB
items.addChild(item)
return response
def _createNotification(self, eventType, service, nodeIdentifier,
subscriber, subscriptions=None):
headers = []
if subscriptions:
for subscription in subscriptions:
if nodeIdentifier != subscription.nodeIdentifier:
headers.append(('Collection', subscription.nodeIdentifier))
message = domish.Element((None, "message"))
message["from"] = service.full()
message["to"] = subscriber.full()
event = message.addElement((NS_PUBSUB_EVENT, "event"))
element = event.addElement(eventType)
element["node"] = nodeIdentifier
if headers:
message.addChild(shim.Headers(headers))
return message
def _toResponse_affiliationsGet(self, result, resource, request):
response = domish.Element((NS_PUBSUB_OWNER, 'pubsub'))
affiliations = response.addElement('affiliations')
if request.nodeIdentifier:
affiliations['node'] = request.nodeIdentifier
for entity, affiliation in result.iteritems():
item = affiliations.addElement('affiliation')
item['jid'] = entity.full()
item['affiliation'] = affiliation
return response
# public methods
def notifyPublish(self, service, nodeIdentifier, notifications):
for subscriber, subscriptions, items in notifications:
message = self._createNotification('items', service,
nodeIdentifier, subscriber,
subscriptions)
for item in items:
item.uri = NS_PUBSUB_EVENT
message.event.items.addChild(item)
self.send(message)
def notifyDelete(self, service, nodeIdentifier, subscribers,
redirectURI=None):
for subscriber in subscribers:
message = self._createNotification('delete', service,
nodeIdentifier,
subscriber)
if redirectURI:
redirect = message.event.delete.addElement('redirect')
redirect['uri'] = redirectURI
self.send(message)
def getNodeInfo(self, requestor, service, nodeIdentifier):
return None
def getNodes(self, requestor, service):
return []
def publish(self, requestor, service, nodeIdentifier, items):
raise Unsupported('publish')
def subscribe(self, requestor, service, nodeIdentifier, subscriber):
raise Unsupported('subscribe')
def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
raise Unsupported('subscribe')
def subscriptions(self, requestor, service):
raise Unsupported('retrieve-subscriptions')
def affiliations(self, requestor, service):
raise Unsupported('retrieve-affiliations')
def create(self, requestor, service, nodeIdentifier):
raise Unsupported('create-nodes')
def getConfigurationOptions(self):
return {}
def getDefaultConfiguration(self, requestor, service, nodeType):
raise Unsupported('retrieve-default')
def getConfiguration(self, requestor, service, nodeIdentifier):
raise Unsupported('config-node')
def setConfiguration(self, requestor, service, nodeIdentifier, options):
raise Unsupported('config-node')
def items(self, requestor, service, nodeIdentifier, maxItems,
itemIdentifiers):
raise Unsupported('retrieve-items')
def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
raise Unsupported('retract-items')
def purge(self, requestor, service, nodeIdentifier):
raise Unsupported('purge-nodes')
def delete(self, requestor, service, nodeIdentifier):
raise Unsupported('delete-nodes')
class PubSubResource(object):
implements(IPubSubResource)
features = []
discoIdentity = disco.DiscoIdentity('pubsub',
'service',
'Publish-Subscribe Service')
def locateResource(self, request):
return self
def getInfo(self, requestor, service, nodeIdentifier):
return defer.succeed(None)
def getNodes(self, requestor, service, nodeIdentifier):
return defer.succeed([])
def getConfigurationOptions(self):
return {}
def publish(self, request):
return defer.fail(Unsupported('publish'))
def subscribe(self, request):
return defer.fail(Unsupported('subscribe'))
def unsubscribe(self, request):
return defer.fail(Unsupported('subscribe'))
def subscriptions(self, request):
return defer.fail(Unsupported('retrieve-subscriptions'))
def affiliations(self, request):
return defer.fail(Unsupported('retrieve-affiliations'))
def create(self, request):
return defer.fail(Unsupported('create-nodes'))
def default(self, request):
return defer.fail(Unsupported('retrieve-default'))
def configureGet(self, request):
return defer.fail(Unsupported('config-node'))
def configureSet(self, request):
return defer.fail(Unsupported('config-node'))
def items(self, request):
return defer.fail(Unsupported('retrieve-items'))
def retract(self, request):
return defer.fail(Unsupported('retract-items'))
def purge(self, request):
return defer.fail(Unsupported('purge-nodes'))
def delete(self, request):
return defer.fail(Unsupported('delete-nodes'))
def affiliationsGet(self, request):
return defer.fail(Unsupported('modify-affiliations'))
def affiliationsSet(self, request):
return defer.fail(Unsupported('modify-affiliations'))
def subscriptionsGet(self, request):
return defer.fail(Unsupported('manage-subscriptions'))
def subscriptionsSet(self, request):
return defer.fail(Unsupported('manage-subscriptions'))
wokkel-0.7.1/wokkel/muc.py 0000664 0001750 0001750 00000131010 11707274572 016247 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_muc -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Multi-User Chat protocol.
This protocol is specified in
U{XEP-0045}.
"""
from dateutil.tz import tzutc
from zope.interface import implements
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, error, xmlstream
from twisted.words.xish import domish
from wokkel import data_form, generic, iwokkel, xmppim
from wokkel.compat import Values, ValueConstant
from wokkel.delay import Delay, DelayMixin
from wokkel.subprotocols import XMPPHandler
from wokkel.iwokkel import IMUCClient
# Multi User Chat namespaces
NS_MUC = 'http://jabber.org/protocol/muc'
NS_MUC_USER = NS_MUC + '#user'
NS_MUC_ADMIN = NS_MUC + '#admin'
NS_MUC_OWNER = NS_MUC + '#owner'
NS_MUC_ROOMINFO = NS_MUC + '#roominfo'
NS_MUC_CONFIG = NS_MUC + '#roomconfig'
NS_MUC_REQUEST = NS_MUC + '#request'
NS_MUC_REGISTER = NS_MUC + '#register'
NS_REGISTER = 'jabber:iq:register'
MESSAGE = '/message'
PRESENCE = '/presence'
GROUPCHAT = MESSAGE +'[@type="groupchat"]'
DEFER_TIMEOUT = 30 # basic timeout is 30 seconds
class STATUS_CODE(Values):
REALJID_PUBLIC = ValueConstant(100)
AFFILIATION_CHANGED = ValueConstant(101)
UNAVAILABLE_SHOWN = ValueConstant(102)
UNAVAILABLE_NOT_SHOWN = ValueConstant(103)
CONFIGURATION_CHANGED = ValueConstant(104)
SELF_PRESENCE = ValueConstant(110)
LOGGING_ENABLED = ValueConstant(170)
LOGGING_DISABLED = ValueConstant(171)
NON_ANONYMOUS = ValueConstant(172)
SEMI_ANONYMOUS = ValueConstant(173)
FULLY_ANONYMOUS = ValueConstant(174)
ROOM_CREATED = ValueConstant(201)
NICK_ASSIGNED = ValueConstant(210)
BANNED = ValueConstant(301)
NEW_NICK = ValueConstant(303)
KICKED = ValueConstant(307)
REMOVED_AFFILIATION = ValueConstant(321)
REMOVED_MEMBERSHIP = ValueConstant(322)
REMOVED_SHUTDOWN = ValueConstant(332)
class Statuses(set):
"""
Container of MUC status conditions.
This is currently implemented as a set of constant values from
L{STATUS_CODE}. Instances of this class provide L{IMUCStatuses}, that
defines the supported operations. Even though this class currently derives
from C{set}, future versions might not. This provides an upgrade path to
cater for extensible status conditions, as defined in
U{XEP-0306}.
"""
implements(iwokkel.IMUCStatuses)
class _FormRequest(generic.Request):
"""
Base class for form exchange requests.
"""
requestNamespace = None
formNamespace = None
def __init__(self, recipient, sender=None, options=None):
if options is None:
stanzaType = 'get'
else:
stanzaType = 'set'
generic.Request.__init__(self, recipient, sender, stanzaType)
self.options = options
def toElement(self):
element = generic.Request.toElement(self)
query = element.addElement((self.requestNamespace, 'query'))
if self.options is None:
# This is a request for the configuration form.
form = None
elif self.options is False:
form = data_form.Form(formType='cancel')
else:
form = data_form.Form(formType='submit',
formNamespace=self.formNamespace)
form.makeFields(self.options)
if form is not None:
query.addChild(form.toElement())
return element
class ConfigureRequest(_FormRequest):
"""
Configure MUC room request.
http://xmpp.org/extensions/xep-0045.html#roomconfig
"""
requestNamespace = NS_MUC_OWNER
formNamespace = NS_MUC_CONFIG
class RegisterRequest(_FormRequest):
"""
Register request.
http://xmpp.org/extensions/xep-0045.html#register
"""
requestNamespace = NS_REGISTER
formNamespace = NS_MUC_REGISTER
class AdminItem(object):
"""
Item representing role and/or affiliation for admin request.
"""
def __init__(self, affiliation=None, role=None, entity=None, nick=None,
reason=None):
self.affiliation = affiliation
self.role = role
self.entity = entity
self.nick = nick
self.reason = reason
def toElement(self):
element = domish.Element((NS_MUC_ADMIN, 'item'))
if self.entity:
element['jid'] = self.entity.full()
if self.nick:
element['nick'] = self.nick
if self.affiliation:
element['affiliation'] = self.affiliation
if self.role:
element['role'] = self.role
if self.reason:
element.addElement('reason', content=self.reason)
return element
@classmethod
def fromElement(Class, element):
item = Class()
if element.hasAttribute('jid'):
item.entity = jid.JID(element['jid'])
item.nick = element.getAttribute('nick')
item.affiliation = element.getAttribute('affiliation')
item.role = element.getAttribute('role')
for child in element.elements(NS_MUC_ADMIN, 'reason'):
item.reason = unicode(child)
return item
class AdminStanza(generic.Request):
"""
An admin request or response.
"""
childParsers = {(NS_MUC_ADMIN, 'query'): '_childParser_query'}
def toElement(self):
element = generic.Request.toElement(self)
element.addElement((NS_MUC_ADMIN, 'query'))
if self.items:
for item in self.items:
element.query.addChild(item.toElement())
return element
def _childParser_query(self, element):
self.items = []
for child in element.elements(NS_MUC_ADMIN, 'item'):
self.items.append(AdminItem.fromElement(child))
class DestructionRequest(generic.Request):
"""
Room destruction request.
@param reason: Optional reason for the destruction of this room.
@type reason: C{unicode}.
@param alternate: Optional room JID of an alternate venue.
@type alternate: L{JID}
@param password: Optional password for entering the alternate venue.
@type password: C{unicode}
"""
stanzaType = 'set'
def __init__(self, recipient, sender=None, reason=None, alternate=None,
password=None):
generic.Request.__init__(self, recipient, sender)
self.reason = reason
self.alternate = alternate
self.password = password
def toElement(self):
element = generic.Request.toElement(self)
element.addElement((NS_MUC_OWNER, 'query'))
element.query.addElement('destroy')
if self.alternate:
element.query.destroy['jid'] = self.alternate.full()
if self.password:
element.query.destroy.addElement('password',
content=self.password)
if self.reason:
element.query.destroy.addElement('reason', content=self.reason)
return element
class GroupChat(xmppim.Message, DelayMixin):
"""
A groupchat message.
"""
stanzaType = 'groupchat'
def toElement(self, legacyDelay=False):
"""
Render into a domish Element.
@param legacyDelay: If C{True} send the delayed delivery information
in legacy format.
"""
element = xmppim.Message.toElement(self)
if self.delay:
element.addChild(self.delay.toElement(legacy=legacyDelay))
return element
class PrivateChat(xmppim.Message):
"""
A chat message.
"""
stanzaType = 'chat'
class InviteMessage(xmppim.Message):
def __init__(self, recipient=None, sender=None, invitee=None, reason=None):
xmppim.Message.__init__(self, recipient, sender)
self.invitee = invitee
self.reason = reason
def toElement(self):
element = xmppim.Message.toElement(self)
child = element.addElement((NS_MUC_USER, 'x'))
child.addElement('invite')
child.invite['to'] = self.invitee.full()
if self.reason:
child.invite.addElement('reason', content=self.reason)
return element
class HistoryOptions(object):
"""
A history configuration object.
@ivar maxchars: Limit the total number of characters in the history to "X"
(where the character count is the characters of the complete XML
stanzas, not only their XML character data).
@type maxchars: C{int}
@ivar maxstanzas: Limit the total number of messages in the history to "X".
@type mazstanzas: C{int}
@ivar seconds: Send only the messages received in the last "X" seconds.
@type seconds: C{int}
@ivar since: Send only the messages received since the datetime specified.
Note that this must be an offset-aware instance.
@type since: L{datetime.datetime}
"""
attributes = ['maxChars', 'maxStanzas', 'seconds', 'since']
def __init__(self, maxChars=None, maxStanzas=None, seconds=None,
since=None):
self.maxChars = maxChars
self.maxStanzas = maxStanzas
self.seconds = seconds
self.since = since
def toElement(self):
"""
Returns a L{domish.Element} representing the history options.
"""
element = domish.Element((NS_MUC, 'history'))
for key in self.attributes:
value = getattr(self, key, None)
if value is not None:
if key == 'since':
stamp = value.astimezone(tzutc())
element[key] = stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
else:
element[key.lower()] = str(value)
return element
class BasicPresence(xmppim.AvailabilityPresence):
"""
Availability presence sent from MUC client to service.
@type history: L{HistoryOptions}
"""
history = None
password = None
def toElement(self):
element = xmppim.AvailabilityPresence.toElement(self)
muc = element.addElement((NS_MUC, 'x'))
if self.password:
muc.addElement('password', content=self.password)
if self.history:
muc.addChild(self.history.toElement())
return element
class UserPresence(xmppim.AvailabilityPresence):
"""
Availability presence sent from MUC service to client.
@ivar affiliation: Affiliation of the entity to the room.
@type affiliation: C{unicode}
@ivar role: Role of the entity in the room.
@type role: C{unicode}
@ivar entity: The real JID of the entity this presence is from.
@type entity: L{JID}
@ivar mucStatuses: Set of one or more status codes from L{STATUS_CODE}.
See L{Statuses} for usage notes.
@type mucStatuses: L{Statuses}
@ivar nick: The nick name of the entity in the room.
@type nick: C{unicode}
"""
affiliation = None
role = None
entity = None
nick = None
mucStatuses = None
childParsers = {(NS_MUC_USER, 'x'): '_childParser_mucUser'}
def __init__(self, *args, **kwargs):
self.mucStatuses = Statuses()
xmppim.AvailabilityPresence.__init__(self, *args, **kwargs)
def _childParser_mucUser(self, element):
"""
Parse the MUC user extension element.
"""
for child in element.elements():
if child.uri != NS_MUC_USER:
continue
elif child.name == 'status':
try:
value = int(child.getAttribute('code'))
statusCode = STATUS_CODE.lookupByValue(value)
except (TypeError, ValueError):
continue
self.mucStatuses.add(statusCode)
elif child.name == 'item':
if child.hasAttribute('jid'):
self.entity = jid.JID(child['jid'])
self.nick = child.getAttribute('nick')
self.affiliation = child.getAttribute('affiliation')
self.role = child.getAttribute('role')
for reason in child.elements(NS_MUC_ADMIN, 'reason'):
self.reason = unicode(reason)
# TODO: destroy
class VoiceRequest(xmppim.Message):
"""
Voice request message.
"""
def toElement(self):
element = xmppim.Message.toElement(self)
# build data form
form = data_form.Form('submit', formNamespace=NS_MUC_REQUEST)
form.addField(data_form.Field(var='muc#role',
value='participant',
label='Requested role'))
element.addChild(form.toElement())
return element
class MUCClientProtocol(xmppim.BasePresenceProtocol):
"""
Multi-User Chat client protocol.
"""
timeout = None
presenceTypeParserMap = {
'error': generic.ErrorStanza,
'available': UserPresence,
'unavailable': UserPresence,
}
def __init__(self, reactor=None):
XMPPHandler.__init__(self)
if reactor:
self._reactor = reactor
else:
from twisted.internet import reactor
self._reactor = reactor
def connectionInitialized(self):
"""
Called when the XML stream has been initialized.
It initializes several XPath events to handle MUC stanzas that come
in.
"""
xmppim.BasePresenceProtocol.connectionInitialized(self)
self.xmlstream.addObserver(GROUPCHAT, self._onGroupChat)
self._roomOccupantMap = {}
def _onGroupChat(self, element):
"""
A group chat message has been received from a MUC room.
There are a few event methods that may get called here.
L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
"""
message = GroupChat.fromElement(element)
self.groupChatReceived(message)
def groupChatReceived(self, message):
"""
Called when a groupchat message was received.
This method is called with a parsed representation of a received
groupchat message and can be overridden for further processing.
For regular groupchat message, the C{body} attribute contains the
message body. Conversation history sent by the room upon joining, will
have the C{delay} attribute set, room subject changes the C{subject}
attribute. See L{GroupChat} for details.
@param message: Groupchat message.
@type message: L{GroupChat}
"""
pass
def _sendDeferred(self, stanza):
"""
Send presence stanza, adding a deferred with a timeout.
@param stanza: The presence stanza to send over the wire.
@type stanza: L{generic.Stanza}
@param timeout: The number of seconds to wait before the deferred is
timed out.
@type timeout: C{int}
The deferred object L{defer.Deferred} is returned.
"""
def onResponse(element):
if element.getAttribute('type') == 'error':
d.errback(error.exceptionFromStanza(element))
else:
d.callback(UserPresence.fromElement(element))
def onTimeout():
d.errback(xmlstream.TimeoutError("Timeout waiting for response."))
def cancelTimeout(result):
if call.active():
call.cancel()
return result
def recordOccupant(presence):
occupantJID = presence.sender
roomJID = occupantJID.userhostJID()
self._roomOccupantMap[roomJID] = occupantJID
return presence
call = self._reactor.callLater(DEFER_TIMEOUT, onTimeout)
d = defer.Deferred()
d.addBoth(cancelTimeout)
d.addCallback(recordOccupant)
query = "/presence[@from='%s' or (@from='%s' and @type='error')]" % (
stanza.recipient.full(), stanza.recipient.userhost())
self.xmlstream.addOnetimeObserver(query, onResponse, priority=-1)
self.xmlstream.send(stanza.toElement())
return d
def join(self, roomJID, nick, historyOptions=None, password=None):
"""
Join a MUC room by sending presence to it.
@param roomJID: The JID of the room the entity is joining.
@type roomJID: L{JID}
@param nick: The nick name for the entitity joining the room.
@type nick: C{unicode}
@param historyOptions: Options for conversation history sent by the
room upon joining.
@type historyOptions: L{HistoryOptions}
@param password: Optional password for the room.
@type password: C{unicode}
@return: A deferred that fires when the entity is in the room or an
error has occurred.
"""
occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick))
presence = BasicPresence(recipient=occupantJID)
if password:
presence.password = password
if historyOptions:
presence.history = historyOptions
return self._sendDeferred(presence)
def nick(self, roomJID, nick):
"""
Change an entity's nick name in a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#changenick
@param roomJID: The JID of the room.
@type roomJID: L{JID}
@param nick: The new nick name within the room.
@type nick: C{unicode}
"""
occupantJID = jid.JID(tuple=(roomJID.user, roomJID.host, nick))
presence = BasicPresence(recipient=occupantJID)
return self._sendDeferred(presence)
def status(self, roomJID, show=None, status=None):
"""
Change user status.
See: http://xmpp.org/extensions/xep-0045.html#changepres
@param roomJID: The Room JID of the room.
@type roomJID: L{JID}
@param show: The availability of the entity. Common values are xa,
available, etc
@type show: C{unicode}
@param status: The current status of the entity.
@type status: C{unicode}
"""
occupantJID = self._roomOccupantMap[roomJID]
presence = BasicPresence(recipient=occupantJID, show=show,
status=status)
return self._sendDeferred(presence)
def leave(self, roomJID):
"""
Leave a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#exit
@param roomJID: The JID of the room.
@type roomJID: L{JID}
"""
occupantJID = self._roomOccupantMap[roomJID]
presence = xmppim.AvailabilityPresence(recipient=occupantJID,
available=False)
return self._sendDeferred(presence)
def groupChat(self, roomJID, body):
"""
Send a groupchat message.
"""
message = GroupChat(recipient=roomJID, body=body)
self.send(message.toElement())
def chat(self, occupantJID, body):
"""
Send a private chat message to a user in a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#privatemessage
@param occupantJID: The Room JID of the other user.
@type occupantJID: L{JID}
"""
message = PrivateChat(recipient=occupantJID, body=body)
self.send(message.toElement())
def subject(self, roomJID, subject):
"""
Change the subject of a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#subject-mod
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param subject: The subject you want to set.
@type subject: C{unicode}
"""
message = GroupChat(roomJID.userhostJID(), subject=subject)
self.send(message.toElement())
def invite(self, roomJID, invitee, reason=None):
"""
Invite a xmpp entity to a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#invite
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param invitee: The entity that is being invited.
@type invitee: L{JID}
@param reason: The reason for the invite.
@type reason: C{unicode}
"""
message = InviteMessage(recipient=roomJID, invitee=invitee,
reason=reason)
self.send(message.toElement())
def getRegisterForm(self, roomJID):
"""
Grab the registration form for a MUC room.
@param room: The room jabber/xmpp entity id for the requested
registration form.
@type room: L{JID}
"""
def cb(response):
form = data_form.findForm(response.query, NS_MUC_REGISTER)
return form
request = RegisterRequest(recipient=roomJID, options=None)
d = self.request(request)
d.addCallback(cb)
return d
def register(self, roomJID, options):
"""
Send a request to register for a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param options: A mapping of field names to values, or C{None} to
cancel.
@type options: C{dict}
"""
if options is None:
options = False
request = RegisterRequest(recipient=roomJID, options=options)
return self.request(request)
def voice(self, roomJID):
"""
Request voice for a moderated room.
@param roomJID: The room jabber/xmpp entity id.
@type roomJID: L{JID}
"""
message = VoiceRequest(recipient=roomJID)
self.xmlstream.send(message.toElement())
def history(self, roomJID, messages):
"""
Send history to create a MUC based on a one on one chat.
See: http://xmpp.org/extensions/xep-0045.html#continue
@param roomJID: The room jabber/xmpp entity id.
@type roomJID: L{JID}
@param messages: The history to send to the room as an ordered list of
message, represented by a dictionary with the keys
C{'stanza'}, holding the original stanza a
L{domish.Element}, and C{'timestamp'} with the
timestamp.
@type messages: C{list} of L{domish.Element}
"""
for message in messages:
stanza = message['stanza']
stanza['type'] = 'groupchat'
delay = Delay(stamp=message['timestamp'])
sender = stanza.getAttribute('from')
if sender is not None:
delay.sender = jid.JID(sender)
stanza.addChild(delay.toElement())
stanza['to'] = roomJID.userhost()
if stanza.hasAttribute('from'):
del stanza['from']
self.xmlstream.send(stanza)
def getConfiguration(self, roomJID):
"""
Grab the configuration from the room.
This sends an iq request to the room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@return: A deferred that fires with the room's configuration form as
a L{data_form.Form} or C{None} if there are no configuration
options available.
"""
def cb(response):
form = data_form.findForm(response.query, NS_MUC_CONFIG)
return form
request = ConfigureRequest(recipient=roomJID, options=None)
d = self.request(request)
d.addCallback(cb)
return d
def configure(self, roomJID, options):
"""
Configure a room.
@param roomJID: The room to configure.
@type roomJID: L{JID}
@param options: A mapping of field names to values, or C{None} to
cancel.
@type options: C{dict}
"""
if options is None:
options = False
request = ConfigureRequest(recipient=roomJID, options=options)
return self.request(request)
def _getAffiliationList(self, roomJID, affiliation):
"""
Send a request for an affiliation list in a room.
"""
def cb(response):
stanza = AdminStanza.fromElement(response)
return stanza.items
request = AdminStanza(recipient=roomJID, stanzaType='get')
request.items = [AdminItem(affiliation=affiliation)]
d = self.request(request)
d.addCallback(cb)
return d
def _getRoleList(self, roomJID, role):
"""
Send a request for a role list in a room.
"""
def cb(response):
stanza = AdminStanza.fromElement(response)
return stanza.items
request = AdminStanza(recipient=roomJID, stanzaType='get')
request.items = [AdminItem(role=role)]
d = self.request(request)
d.addCallback(cb)
return d
def getMemberList(self, roomJID):
"""
Get the member list of a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
"""
return self._getAffiliationList(roomJID, 'member')
def getAdminList(self, roomJID):
"""
Get the admin list of a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
"""
return self._getAffiliationList(roomJID, 'admin')
def getBanList(self, roomJID):
"""
Get an outcast list from a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
"""
return self._getAffiliationList(roomJID, 'outcast')
def getOwnerList(self, roomJID):
"""
Get an owner list from a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
"""
return self._getAffiliationList(roomJID, 'owner')
def getModeratorList(self, roomJID):
"""
Get the moderator list of a room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
"""
d = self._getRoleList(roomJID, 'moderator')
return d
def _setAffiliation(self, roomJID, entity, affiliation,
reason=None, sender=None):
"""
Send a request to change an entity's affiliation to a MUC room.
"""
request = AdminStanza(recipient=roomJID, sender=sender,
stanzaType='set')
item = AdminItem(entity=entity, affiliation=affiliation, reason=reason)
request.items = [item]
return self.request(request)
def _setRole(self, roomJID, nick, role,
reason=None, sender=None):
"""
Send a request to change an occupant's role in a MUC room.
"""
request = AdminStanza(recipient=roomJID, sender=sender,
stanzaType='set')
item = AdminItem(nick=nick, role=role, reason=reason)
request.items = [item]
return self.request(request)
def modifyAffiliationList(self, roomJID, entities, affiliation,
sender=None):
"""
Modify an affiliation list.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param entities: The list of entities to change for a room.
@type entities: C{list} of
L{JID}
@param affiliation: The affilation to the entities will acquire.
@type affiliation: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
request = AdminStanza(recipient=roomJID, sender=sender,
stanzaType='set')
request.items = [AdminItem(entity=entity, affiliation=affiliation)
for entity in entities]
return self.request(request)
def grantVoice(self, roomJID, nick, reason=None, sender=None):
"""
Grant voice to an entity.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param nick: The nick name for the user in this room.
@type nick: C{unicode}
@param reason: The reason for granting voice to the entity.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
return self._setRole(roomJID, nick=nick,
role='participant',
reason=reason, sender=sender)
def revokeVoice(self, roomJID, nick, reason=None, sender=None):
"""
Revoke voice from a participant.
This will disallow the entity to send messages to a moderated room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param nick: The nick name for the user in this room.
@type nick: C{unicode}
@param reason: The reason for revoking voice from the entity.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
return self._setRole(roomJID, nick=nick, role='visitor',
reason=reason, sender=sender)
def grantModerator(self, roomJID, nick, reason=None, sender=None):
"""
Grant moderator privileges to a MUC room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param nick: The nick name for the user in this room.
@type nick: C{unicode}
@param reason: The reason for granting moderation to the entity.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
return self._setRole(roomJID, nick=nick, role='moderator',
reason=reason, sender=sender)
def ban(self, roomJID, entity, reason=None, sender=None):
"""
Ban a user from a MUC room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param entity: The bare JID of the entity to be banned.
@type entity: L{JID}
@param reason: The reason for banning the entity.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
return self._setAffiliation(roomJID, entity, 'outcast',
reason=reason, sender=sender)
def kick(self, roomJID, nick, reason=None, sender=None):
"""
Kick a user from a MUC room.
@param roomJID: The bare JID of the room.
@type roomJID: L{JID}
@param nick: The occupant to be banned.
@type nick: C{unicode}
@param reason: The reason given for the kick.
@type reason: C{unicode}
@param sender: The entity sending the request.
@type sender: L{JID}
"""
return self._setRole(roomJID, nick, 'none',
reason=reason, sender=sender)
def destroy(self, roomJID, reason=None, alternate=None, password=None):
"""
Destroy a room.
@param roomJID: The JID of the room.
@type roomJID: L{JID}
@param reason: The reason for the destruction of the room.
@type reason: C{unicode}
@param alternate: The JID of the room suggested as an alternate venue.
@type alternate: L{JID}
"""
request = DestructionRequest(recipient=roomJID, reason=reason,
alternate=alternate, password=password)
return self.request(request)
class User(object):
"""
A user/entity in a multi-user chat room.
"""
def __init__(self, nick, entity=None):
self.nick = nick
self.entity = entity
self.affiliation = 'none'
self.role = 'none'
self.status = None
self.show = None
class Room(object):
"""
A Multi User Chat Room.
An in memory object representing a MUC room from the perspective of
a client.
@ivar roomJID: The Room JID of the MUC room.
@type roomJID: L{JID}
@ivar nick: The nick name for the client in this room.
@type nick: C{unicode}
@ivar occupantJID: The JID of the occupant in the room. Generated from
roomJID and nick.
@type occupantJID: L{JID}
@ivar locked: Flag signalling a locked room. A locked room first needs
to be configured before it can be used. See
L{MUCClientProtocol.getConfiguration} and
L{MUCClientProtocol.configure}.
@type locked: C{bool}
"""
locked = False
def __init__(self, roomJID, nick):
"""
Initialize the room.
"""
self.roomJID = roomJID
self.setNick(nick)
self.roster = {}
def setNick(self, nick):
self.occupantJID = jid.internJID(u"%s/%s" % (self.roomJID, nick))
self.nick = nick
def addUser(self, user):
"""
Add a user to the room roster.
@param user: The user object that is being added to the room.
@type user: L{User}
"""
self.roster[user.nick] = user
def inRoster(self, user):
"""
Check if a user is in the MUC room.
@param user: The user object to check.
@type user: L{User}
"""
return user.nick in self.roster
def getUser(self, nick):
"""
Get a user from the room's roster.
@param nick: The nick for the user in the MUC room.
@type nick: C{unicode}
"""
return self.roster.get(nick)
def removeUser(self, user):
"""
Remove a user from the MUC room's roster.
@param user: The user object to check.
@type user: L{User}
"""
if self.inRoster(user):
del self.roster[user.nick]
class MUCClient(MUCClientProtocol):
"""
Multi-User Chat client protocol.
This is a subclass of L{XMPPHandler} and implements L{IMUCClient}.
@ivar _rooms: Collection of occupied rooms, keyed by the bare JID of the
room. Note that a particular entity can only join a room once
at a time.
@type _rooms: C{dict}
"""
implements(IMUCClient)
def __init__(self, reactor=None):
MUCClientProtocol.__init__(self, reactor)
self._rooms = {}
def _addRoom(self, room):
"""
Add a room to the room collection.
Rooms are stored by the JID of the room itself. I.e. it uses the Room
ID and service parts of the Room JID.
@note: An entity can only join a particular room once.
"""
roomJID = room.occupantJID.userhostJID()
self._rooms[roomJID] = room
def _getRoom(self, roomJID):
"""
Grab a room from the room collection.
This uses the Room ID and service parts of the given JID to look up
the L{Room} instance associated with it.
@type occupantJID: L{JID}
"""
return self._rooms.get(roomJID)
def _removeRoom(self, roomJID):
"""
Delete a room from the room collection.
"""
if roomJID in self._rooms:
del self._rooms[roomJID]
def _getRoomUser(self, stanza):
"""
Lookup the room and user associated with the stanza's sender.
"""
occupantJID = stanza.sender
if not occupantJID:
return None, None
# when a user leaves a room we need to update it
room = self._getRoom(occupantJID.userhostJID())
if room is None:
# not in the room yet
return None, None
# Check if user is in roster
nick = occupantJID.resource
user = room.getUser(nick)
return room, user
def unavailableReceived(self, presence):
"""
Unavailable presence was received.
If this was received from a MUC room occupant JID, that occupant has
left the room.
"""
room, user = self._getRoomUser(presence)
if room is None or user is None:
return
room.removeUser(user)
self.userLeftRoom(room, user)
def availableReceived(self, presence):
"""
Available presence was received.
"""
room, user = self._getRoomUser(presence)
if room is None:
return
if user is None:
nick = presence.sender.resource
user = User(nick, presence.entity)
# Update user status
user.status = presence.status
user.show = presence.show
if room.inRoster(user):
self.userUpdatedStatus(room, user, presence.show, presence.status)
else:
room.addUser(user)
self.userJoinedRoom(room, user)
def groupChatReceived(self, message):
"""
A group chat message has been received from a MUC room.
There are a few event methods that may get called here.
L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
"""
room, user = self._getRoomUser(message)
if room is None:
return
if message.subject:
self.receivedSubject(room, user, message.subject)
elif message.delay is None:
self.receivedGroupChat(room, user, message)
else:
self.receivedHistory(room, user, message)
def userJoinedRoom(self, room, user):
"""
User has joined a MUC room.
This method will need to be modified inorder for clients to
do something when this event occurs.
@param room: The room the user has joined.
@type room: L{Room}
@param user: The user that joined the MUC room.
@type user: L{User}
"""
pass
def userLeftRoom(self, room, user):
"""
User has left a room.
This method will need to be modified inorder for clients to
do something when this event occurs.
@param room: The room the user has joined.
@type room: L{Room}
@param user: The user that left the MUC room.
@type user: L{User}
"""
pass
def userUpdatedStatus(self, room, user, show, status):
"""
User Presence has been received.
This method will need to be modified inorder for clients to
do something when this event occurs.
"""
pass
def receivedSubject(self, room, user, subject):
"""
A (new) room subject has been received.
This method will need to be modified inorder for clients to
do something when this event occurs.
"""
pass
def receivedGroupChat(self, room, user, message):
"""
A groupchat message was received.
@param room: The room the message was received from.
@type room: L{Room}
@param user: The user that sent the message, or C{None} if it was a
message from the room itself.
@type user: L{User}
@param message: The message.
@type message: L{GroupChat}
"""
pass
def receivedHistory(self, room, user, message):
"""
A groupchat message from the room's discussion history was received.
This is identical to L{receivedGroupChat}, with the delayed delivery
information (timestamp and original sender) in C{message.delay}. For
anonymous rooms, C{message.delay.sender} is the room's address.
@param room: The room the message was received from.
@type room: L{Room}
@param user: The user that sent the message, or C{None} if it was a
message from the room itself.
@type user: L{User}
@param message: The message.
@type message: L{GroupChat}
"""
pass
def join(self, roomJID, nick, historyOptions=None,
password=None):
"""
Join a MUC room by sending presence to it.
@param roomJID: The JID of the room the entity is joining.
@type roomJID: L{JID}
@param nick: The nick name for the entitity joining the room.
@type nick: C{unicode}
@param historyOptions: Options for conversation history sent by the
room upon joining.
@type historyOptions: L{HistoryOptions}
@param password: Optional password for the room.
@type password: C{unicode}
@return: A deferred that fires with the room when the entity is in the
room, or with a failure if an error has occurred.
"""
def cb(presence):
"""
We have presence that says we joined a room.
"""
if STATUS_CODE.ROOM_CREATED in presence.mucStatuses:
room.locked = True
return room
def eb(failure):
self._removeRoom(roomJID)
return failure
room = Room(roomJID, nick)
self._addRoom(room)
d = MUCClientProtocol.join(self, roomJID, nick, historyOptions,
password)
d.addCallbacks(cb, eb)
return d
def nick(self, roomJID, nick):
"""
Change an entity's nick name in a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#changenick
@param roomJID: The JID of the room, i.e. without a resource.
@type roomJID: L{JID}
@param nick: The new nick name within the room.
@type nick: C{unicode}
"""
def cb(presence):
# Presence confirmation, change the nickname.
room.setNick(nick)
return room
room = self._getRoom(roomJID)
d = MUCClientProtocol.nick(self, roomJID, nick)
d.addCallback(cb)
return d
def leave(self, roomJID):
"""
Leave a MUC room.
See: http://xmpp.org/extensions/xep-0045.html#exit
@param roomJID: The Room JID of the room to leave.
@type roomJID: L{JID}
"""
def cb(presence):
self._removeRoom(roomJID)
d = MUCClientProtocol.leave(self, roomJID)
d.addCallback(cb)
return d
def status(self, roomJID, show=None, status=None):
"""
Change user status.
See: http://xmpp.org/extensions/xep-0045.html#changepres
@param roomJID: The Room JID of the room.
@type roomJID: L{JID}
@param show: The availability of the entity. Common values are xa,
available, etc
@type show: C{unicode}
@param status: The current status of the entity.
@type status: C{unicode}
"""
room = self._getRoom(roomJID)
d = MUCClientProtocol.status(self, roomJID, show, status)
d.addCallback(lambda _: room)
return d
def destroy(self, roomJID, reason=None, alternate=None, password=None):
"""
Destroy a room.
@param roomJID: The JID of the room.
@type roomJID: L{JID}
@param reason: The reason for the destruction of the room.
@type reason: C{unicode}
@param alternate: The JID of the room suggested as an alternate venue.
@type alternate: L{JID}
"""
def destroyed(iq):
self._removeRoom(roomJID)
d = MUCClientProtocol.destroy(self, roomJID, reason, alternate)
d.addCallback(destroyed)
return d
wokkel-0.7.1/wokkel/xmppim.py 0000775 0001750 0001750 00000101567 12074336410 017002 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_xmppim -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP IM protocol support.
This module provides generic implementations for the protocols defined in
U{RFC 6121} (XMPP IM).
"""
import warnings
from twisted.internet import defer
from twisted.words.protocols.jabber import error
from twisted.words.protocols.jabber.jid import JID
from twisted.words.xish import domish
from wokkel.generic import ErrorStanza, Stanza, Request
from wokkel.subprotocols import IQHandlerMixin
from wokkel.subprotocols import XMPPHandler
NS_XML = 'http://www.w3.org/XML/1998/namespace'
NS_ROSTER = 'jabber:iq:roster'
XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
class Presence(domish.Element):
def __init__(self, to=None, type=None):
domish.Element.__init__(self, (None, "presence"))
if type:
self["type"] = type
if to is not None:
self["to"] = to.full()
class AvailablePresence(Presence):
def __init__(self, to=None, show=None, statuses=None, priority=0):
Presence.__init__(self, to, type=None)
if show in ['away', 'xa', 'chat', 'dnd']:
self.addElement('show', content=show)
if statuses is not None:
for lang, status in statuses.iteritems():
s = self.addElement('status', content=status)
if lang:
s[(NS_XML, "lang")] = lang
if priority != 0:
self.addElement('priority', content=unicode(int(priority)))
class UnavailablePresence(Presence):
def __init__(self, to=None, statuses=None):
Presence.__init__(self, to, type='unavailable')
if statuses is not None:
for lang, status in statuses.iteritems():
s = self.addElement('status', content=status)
if lang:
s[(NS_XML, "lang")] = lang
class PresenceClientProtocol(XMPPHandler):
def connectionInitialized(self):
self.xmlstream.addObserver('/presence', self._onPresence)
def _getStatuses(self, presence):
statuses = {}
for element in presence.elements():
if element.name == 'status':
lang = element.getAttribute((NS_XML, 'lang'))
text = unicode(element)
statuses[lang] = text
return statuses
def _onPresence(self, presence):
type = presence.getAttribute("type", "available")
try:
handler = getattr(self, '_onPresence%s' % (type.capitalize()))
except AttributeError:
return
else:
handler(presence)
def _onPresenceAvailable(self, presence):
entity = JID(presence["from"])
show = unicode(presence.show or '')
if show not in ['away', 'xa', 'chat', 'dnd']:
show = None
statuses = self._getStatuses(presence)
try:
priority = int(unicode(presence.priority or '')) or 0
except ValueError:
priority = 0
self.availableReceived(entity, show, statuses, priority)
def _onPresenceUnavailable(self, presence):
entity = JID(presence["from"])
statuses = self._getStatuses(presence)
self.unavailableReceived(entity, statuses)
def _onPresenceSubscribed(self, presence):
self.subscribedReceived(JID(presence["from"]))
def _onPresenceUnsubscribed(self, presence):
self.unsubscribedReceived(JID(presence["from"]))
def _onPresenceSubscribe(self, presence):
self.subscribeReceived(JID(presence["from"]))
def _onPresenceUnsubscribe(self, presence):
self.unsubscribeReceived(JID(presence["from"]))
def availableReceived(self, entity, show=None, statuses=None, priority=0):
"""
Available presence was received.
@param entity: entity from which the presence was received.
@type entity: {JID}
@param show: detailed presence information. One of C{'away'}, C{'xa'},
C{'chat'}, C{'dnd'} or C{None}.
@type show: C{str} or C{NoneType}
@param statuses: dictionary of natural language descriptions of the
availability status, keyed by the language
descriptor. A status without a language
specified, is keyed with C{None}.
@type statuses: C{dict}
@param priority: priority level of the resource.
@type priority: C{int}
"""
def unavailableReceived(self, entity, statuses=None):
"""
Unavailable presence was received.
@param entity: entity from which the presence was received.
@type entity: {JID}
@param statuses: dictionary of natural language descriptions of the
availability status, keyed by the language
descriptor. A status without a language
specified, is keyed with C{None}.
@type statuses: C{dict}
"""
def subscribedReceived(self, entity):
"""
Subscription approval confirmation was received.
@param entity: entity from which the confirmation was received.
@type entity: {JID}
"""
def unsubscribedReceived(self, entity):
"""
Unsubscription confirmation was received.
@param entity: entity from which the confirmation was received.
@type entity: {JID}
"""
def subscribeReceived(self, entity):
"""
Subscription request was received.
@param entity: entity from which the request was received.
@type entity: {JID}
"""
def unsubscribeReceived(self, entity):
"""
Unsubscription request was received.
@param entity: entity from which the request was received.
@type entity: {JID}
"""
def available(self, entity=None, show=None, statuses=None, priority=0):
"""
Send available presence.
@param entity: optional entity to which the presence should be sent.
@type entity: {JID}
@param show: optional detailed presence information. One of C{'away'},
C{'xa'}, C{'chat'}, C{'dnd'}.
@type show: C{str}
@param statuses: dictionary of natural language descriptions of the
availability status, keyed by the language
descriptor. A status without a language
specified, is keyed with C{None}.
@type statuses: C{dict}
@param priority: priority level of the resource.
@type priority: C{int}
"""
self.send(AvailablePresence(entity, show, statuses, priority))
def unavailable(self, entity=None, statuses=None):
"""
Send unavailable presence.
@param entity: optional entity to which the presence should be sent.
@type entity: {JID}
@param statuses: dictionary of natural language descriptions of the
availability status, keyed by the language
descriptor. A status without a language
specified, is keyed with C{None}.
@type statuses: C{dict}
"""
self.send(UnavailablePresence(entity, statuses))
def subscribe(self, entity):
"""
Send subscription request
@param entity: entity to subscribe to.
@type entity: {JID}
"""
self.send(Presence(to=entity, type='subscribe'))
def unsubscribe(self, entity):
"""
Send unsubscription request
@param entity: entity to unsubscribe from.
@type entity: {JID}
"""
self.send(Presence(to=entity, type='unsubscribe'))
def subscribed(self, entity):
"""
Send subscription confirmation.
@param entity: entity that subscribed.
@type entity: {JID}
"""
self.send(Presence(to=entity, type='subscribed'))
def unsubscribed(self, entity):
"""
Send unsubscription confirmation.
@param entity: entity that unsubscribed.
@type entity: {JID}
"""
self.send(Presence(to=entity, type='unsubscribed'))
class BasePresence(Stanza):
"""
Stanza of kind presence.
"""
stanzaKind = 'presence'
class AvailabilityPresence(BasePresence):
"""
Presence.
This represents availability presence (as opposed to
L{SubscriptionPresence}).
@ivar available: The availability being communicated.
@type available: C{bool}
@ivar show: More specific availability. Can be one of C{'chat'}, C{'away'},
C{'xa'}, C{'dnd'} or C{None}.
@type show: C{str} or C{NoneType}
@ivar statuses: Natural language texts to detail the (un)availability.
These are represented as a mapping from language code
(C{str} or C{None}) to the corresponding text (C{unicode}).
If the key is C{None}, the associated text is in the
default language.
@type statuses: C{dict}
@ivar priority: Priority level for this resource. Must be between -128 and
127. Defaults to 0.
@type priority: C{int}
"""
childParsers = {(None, 'show'): '_childParser_show',
(None, 'status'): '_childParser_status',
(None, 'priority'): '_childParser_priority'}
def __init__(self, recipient=None, sender=None, available=True,
show=None, status=None, statuses=None, priority=0):
BasePresence.__init__(self, recipient=recipient, sender=sender)
self.available = available
self.show = show
self.statuses = statuses or {}
if status:
self.statuses[None] = status
self.priority = priority
def __get_status(self):
if None in self.statuses:
return self.statuses[None]
elif self.statuses:
for status in self.status.itervalues():
return status
else:
return None
status = property(__get_status)
def _childParser_show(self, element):
show = unicode(element)
if show in ('chat', 'away', 'xa', 'dnd'):
self.show = show
def _childParser_status(self, element):
lang = element.getAttribute((NS_XML, 'lang'), None)
text = unicode(element)
self.statuses[lang] = text
def _childParser_priority(self, element):
try:
self.priority = int(unicode(element))
except ValueError:
pass
def parseElement(self, element):
BasePresence.parseElement(self, element)
if self.stanzaType == 'unavailable':
self.available = False
def toElement(self):
if not self.available:
self.stanzaType = 'unavailable'
presence = BasePresence.toElement(self)
if self.available:
if self.show in ('chat', 'away', 'xa', 'dnd'):
presence.addElement('show', content=self.show)
if self.priority != 0:
presence.addElement('priority', content=unicode(self.priority))
for lang, text in self.statuses.iteritems():
status = presence.addElement('status', content=text)
if lang:
status[(NS_XML, 'lang')] = lang
return presence
class SubscriptionPresence(BasePresence):
"""
Presence subscription request or response.
This kind of presence is used to represent requests for presence
subscription and their replies.
Based on L{BasePresence} and {Stanza}, it just uses the C{stanzaType}
attribute to represent the type of subscription presence. This can be
one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and
C{'unsubscribed'}.
"""
class ProbePresence(BasePresence):
"""
Presence probe request.
"""
stanzaType = 'probe'
class BasePresenceProtocol(XMPPHandler):
"""
XMPP Presence base protocol handler.
This class is the base for protocol handlers that receive presence
stanzas. Listening to all incoming presence stanzas, it extracts the
stanza's type and looks up a matching stanza parser and calls the
associated method. The method's name is the type + C{Received}. E.g.
C{availableReceived}. See L{PresenceProtocol} for a complete example.
@cvar presenceTypeParserMap: Maps presence stanza types to their respective
stanza parser classes (derived from L{Stanza}).
@type presenceTypeParserMap: C{dict}
"""
presenceTypeParserMap = {}
def connectionInitialized(self):
self.xmlstream.addObserver("/presence", self._onPresence)
def _onPresence(self, element):
"""
Called when a presence stanza has been received.
"""
stanza = Stanza.fromElement(element)
presenceType = stanza.stanzaType or 'available'
try:
parser = self.presenceTypeParserMap[presenceType]
except KeyError:
return
presence = parser.fromElement(element)
try:
handler = getattr(self, '%sReceived' % presenceType)
except AttributeError:
return
else:
handler(presence)
class PresenceProtocol(BasePresenceProtocol):
presenceTypeParserMap = {
'error': ErrorStanza,
'available': AvailabilityPresence,
'unavailable': AvailabilityPresence,
'subscribe': SubscriptionPresence,
'unsubscribe': SubscriptionPresence,
'subscribed': SubscriptionPresence,
'unsubscribed': SubscriptionPresence,
'probe': ProbePresence,
}
def errorReceived(self, presence):
"""
Error presence was received.
"""
pass
def availableReceived(self, presence):
"""
Available presence was received.
"""
pass
def unavailableReceived(self, presence):
"""
Unavailable presence was received.
"""
pass
def subscribedReceived(self, presence):
"""
Subscription approval confirmation was received.
"""
pass
def unsubscribedReceived(self, presence):
"""
Unsubscription confirmation was received.
"""
pass
def subscribeReceived(self, presence):
"""
Subscription request was received.
"""
pass
def unsubscribeReceived(self, presence):
"""
Unsubscription request was received.
"""
pass
def probeReceived(self, presence):
"""
Probe presence was received.
"""
pass
def available(self, recipient=None, show=None, statuses=None, priority=0,
status=None, sender=None):
"""
Send available presence.
@param recipient: Optional Recipient to which the presence should be
sent.
@type recipient: {JID}
@param show: Optional detailed presence information. One of C{'away'},
C{'xa'}, C{'chat'}, C{'dnd'}.
@type show: C{str}
@param statuses: Mapping of natural language descriptions of the
availability status, keyed by the language descriptor. A status
without a language specified, is keyed with C{None}.
@type statuses: C{dict}
@param priority: priority level of the resource.
@type priority: C{int}
"""
presence = AvailabilityPresence(recipient=recipient, sender=sender,
show=show, statuses=statuses,
status=status, priority=priority)
self.send(presence.toElement())
def unavailable(self, recipient=None, statuses=None, sender=None):
"""
Send unavailable presence.
@param recipient: Optional entity to which the presence should be sent.
@type recipient: {JID}
@param statuses: dictionary of natural language descriptions of the
availability status, keyed by the language descriptor. A status
without a language specified, is keyed with C{None}.
@type statuses: C{dict}
"""
presence = AvailabilityPresence(recipient=recipient, sender=sender,
available=False, statuses=statuses)
self.send(presence.toElement())
def subscribe(self, recipient, sender=None):
"""
Send subscription request
@param recipient: Entity to subscribe to.
@type recipient: {JID}
"""
presence = SubscriptionPresence(recipient=recipient, sender=sender)
presence.stanzaType = 'subscribe'
self.send(presence.toElement())
def unsubscribe(self, recipient, sender=None):
"""
Send unsubscription request
@param recipient: Entity to unsubscribe from.
@type recipient: {JID}
"""
presence = SubscriptionPresence(recipient=recipient, sender=sender)
presence.stanzaType = 'unsubscribe'
self.send(presence.toElement())
def subscribed(self, recipient, sender=None):
"""
Send subscription confirmation.
@param recipient: Entity that subscribed.
@type recipient: {JID}
"""
presence = SubscriptionPresence(recipient=recipient, sender=sender)
presence.stanzaType = 'subscribed'
self.send(presence.toElement())
def unsubscribed(self, recipient, sender=None):
"""
Send unsubscription confirmation.
@param recipient: Entity that unsubscribed.
@type recipient: {JID}
"""
presence = SubscriptionPresence(recipient=recipient, sender=sender)
presence.stanzaType = 'unsubscribed'
self.send(presence.toElement())
def probe(self, recipient, sender=None):
"""
Send presence probe.
@param recipient: Entity to be probed.
@type recipient: {JID}
"""
presence = ProbePresence(recipient=recipient, sender=sender)
self.send(presence.toElement())
class RosterItem(object):
"""
Roster item.
This represents one contact from an XMPP contact list known as roster.
@ivar entity: The JID of the contact.
@type entity: L{JID}
@ivar name: The associated nickname for this contact.
@type name: C{unicode}
@ivar subscriptionTo: Subscription state to contact's presence. If C{True},
the roster owner is subscribed to the presence
information of the contact.
@type subscriptionTo: C{bool}
@ivar subscriptionFrom: Contact's subscription state. If C{True}, the
contact is subscribed to the presence information
of the roster owner.
@type subscriptionFrom: C{bool}
@ivar pendingOut: Whether the subscription request to this contact is
pending.
@type pendingOut: C{bool}
@ivar groups: Set of groups this contact is categorized in. Groups are
represented by an opaque identifier of type C{unicode}.
@type groups: C{set}
@ivar approved: Signals pre-approved subscription.
@type approved: C{bool}
@ivar remove: Signals roster item removal.
@type remove: C{bool}
"""
__subscriptionStates = {(False, False): None,
(True, False): 'to',
(False, True): 'from',
(True, True): 'both'}
def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
name=u'', groups=None):
self.entity = entity
self.subscriptionTo = subscriptionTo
self.subscriptionFrom = subscriptionFrom
self.name = name
self.groups = groups or set()
self.pendingOut = False
self.approved = False
self.remove = False
def __getJID(self):
warnings.warn(
"wokkel.xmppim.RosterItem.jid was deprecated in Wokkel 0.7.1; "
"please use RosterItem.entity instead.",
DeprecationWarning)
return self.entity
def __setJID(self, value):
warnings.warn(
"wokkel.xmppim.RosterItem.jid was deprecated in Wokkel 0.7.1; "
"please use RosterItem.entity instead.",
DeprecationWarning)
self.entity = value
jid = property(__getJID, __setJID,
doc="JID of the contact. "
"Deprecated in Wokkel 0.7.1; "
"please use C{entity} instead.")
def __getAsk(self):
warnings.warn(
"wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; "
"please use RosterItem.pendingOut instead.",
DeprecationWarning)
return self.pendingOut
def __setAsk(self, value):
warnings.warn(
"wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; "
"please use RosterItem.pendingOut instead.",
DeprecationWarning)
self.pendingOut = value
ask = property(__getAsk, __setAsk,
doc="Pending out subscription. "
"Deprecated in Wokkel 0.7.1; "
"please use C{pendingOut} instead.")
def toElement(self, rosterSet=False):
"""
Render to a DOM representation.
If C{rosterSet} is set, some attributes, that may not be sent
as a roster set, will not be rendered.
@type rosterSet: C{boolean}.
"""
element = domish.Element((NS_ROSTER, 'item'))
element['jid'] = self.entity.full()
if self.remove:
subscription = 'remove'
else:
if self.name:
element['name'] = self.name
if self.groups:
for group in self.groups:
element.addElement('group', content=group)
if rosterSet:
subscription = None
else:
subscription = self.__subscriptionStates[self.subscriptionTo,
self.subscriptionFrom]
if self.pendingOut:
element['ask'] = u'subscribe'
if self.approved:
element['approved'] = u'true'
if subscription:
element['subscription'] = subscription
return element
@classmethod
def fromElement(Class, element):
entity = JID(element['jid'])
item = Class(entity)
subscription = element.getAttribute('subscription')
if subscription == 'remove':
item.remove = True
else:
item.name = element.getAttribute('name', u'')
item.subscriptionTo = subscription in ('to', 'both')
item.subscriptionFrom = subscription in ('from', 'both')
item.pendingOut = element.getAttribute('ask') == 'subscribe'
item.approved = element.getAttribute('approved') in ('true', '1')
for subElement in domish.generateElementsQNamed(element.children,
'group', NS_ROSTER):
item.groups.add(unicode(subElement))
return item
class RosterRequest(Request):
"""
Roster request.
@ivar item: Roster item to be set or pushed.
@type item: L{RosterItem}.
@ivar version: Roster version identifier for roster pushes and
retrieving the roster as a delta from a known cached version. This
should only be set if the recipient is known to support roster
versioning.
@type version: C{unicode}
@ivar rosterSet: If set, this is a roster set request. This flag is used
to make sure some attributes of the roster item are not rendered by
L{toElement}.
@type roster: C{boolean}
"""
item = None
version = None
rosterSet = False
def parseRequest(self, element):
self.version = element.getAttribute('ver')
for child in element.elements(NS_ROSTER, 'item'):
self.item = RosterItem.fromElement(child)
break
def toElement(self):
element = Request.toElement(self)
query = element.addElement((NS_ROSTER, 'query'))
if self.version is not None:
query['ver'] = self.version
if self.item:
query.addChild(self.item.toElement(rosterSet=self.rosterSet))
return element
class RosterPushIgnored(Exception):
"""
Raised when this entity doesn't want to accept/trust a roster push.
To avert presence leaks, a handler can raise L{RosterPushIgnored} when
not accepting a roster push (directly or via Deferred). This will
result in a C{'service-unavailable'} error being sent in return.
"""
class Roster(dict):
"""
In-memory roster container.
This provides a roster as a mapping from L{JID} to L{RosterItem}. If
roster versioning is used, the C{version} attribute holds the version
identifier for this version of the roster.
@ivar version: Roster version identifier.
@type version: C{unicode}.
"""
version = None
class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
"""
Client side XMPP roster protocol.
The roster can be retrieved using L{getRoster}. Subsequent changes to the
roster will be pushed, resulting in calls to L{setReceived} or
L{removeReceived}. These methods should be overridden to handle the
roster pushes.
RFC 6121 specifically allows entities other than a user's server to
hold a roster for that user. However, how a client should deal with
that is currently not yet specfied.
By default roster pushes from other source. I.e. when C{request.sender}
is set but the sender's bare JID is different from the user's bare JID.
Set L{allowAnySender} to allow roster pushes from any sender. To
avert presence leaks, L{RosterPushIgnored} should then be raised for
pushes from untrusted senders.
If roster versioning is supported by the server, the roster and
subsequent pushes are annotated with a version identifier. This can be
used to cache the roster on the client side. Upon reconnect, the client
can request the roster with the version identifier of the cached version.
The server may then choose to only send roster pushes for the changes
since that version, instead of a complete roster.
@cvar allowAnySender: Flag to allow roster pushes from any sender.
C{False} by default.
@type allowAnySender: C{boolean}
"""
allowAnySender = False
iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
def connectionInitialized(self):
self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
def getRoster(self, version=None):
"""
Retrieve contact list.
The returned deferred fires with the result of the roster request as
L{Roster}, a mapping from contact JID to L{RosterItem}.
If roster versioning is supported, the recipient responds with either
a the complete roster or with an empty result. In case of complete
roster, the L{Roster} is annotated with a C{version} attribute that
holds the version identifier for this version of the roster. This
identifier should be used for caching.
If the recipient responds with an empty result, the returned deferred
fires with C{None}. This indicates that any roster modifications
since C{version} will be sent as roster pushes.
Note that the empty result (C{None}) is different from an empty
roster (L{Roster} with no items).
@param version: Optional version identifier of the last cashed
version of the roster. This shall only be set if the recipient is
known to support roster versioning. If there is no (valid) cached
version of the roster, but roster versioning is desired,
C{version} should be set to the empty string (C{u''}).
@type version: C{unicode}
@return: Roster as a mapping from L{JID} to L{RosterItem}.
@rtype: L{twisted.internet.defer.Deferred}
"""
def processRoster(result):
if result.query is not None:
roster = Roster()
roster.version = result.query.getAttribute('ver')
for element in result.query.elements(NS_ROSTER, 'item'):
item = RosterItem.fromElement(element)
roster[item.entity] = item
return roster
else:
return None
request = RosterRequest(stanzaType='get')
request.version = version
d = self.request(request)
d.addCallback(processRoster)
return d
def setItem(self, item):
"""
Add or modify a roster item.
Note that RFC 6121 doesn't allow all properties of a roster item to
be sent when setting a roster item. Only the C{name} and C{groups}
attributes from C{item} are sent to the server. Presence subscription
management must be done through L{PresenceProtocol}.
@param item: The roster item to be set.
@type item: L{RosterItem}.
@rtype: L{twisted.internet.defer.Deferred}
"""
request = RosterRequest(stanzaType='set')
request.rosterSet = True
request.item = item
return self.request(request)
def removeItem(self, entity):
"""
Remove an item from the contact list.
@param entity: The contact to remove the roster item for.
@type entity: L{JID}
@rtype: L{twisted.internet.defer.Deferred}
"""
item = RosterItem(entity)
item.remove = True
return self.setItem(item)
def _onRosterSet(self, iq):
def trapIgnored(failure):
failure.trap(RosterPushIgnored)
raise error.StanzaError('service-unavailable')
request = RosterRequest.fromElement(iq)
if (not self.allowAnySender and
request.sender and
request.sender.userhostJID() !=
self.parent.jid.userhostJID()):
d = defer.fail(RosterPushIgnored())
elif request.item.remove:
d = defer.maybeDeferred(self.removeReceived, request)
else:
d = defer.maybeDeferred(self.setReceived, request)
d.addErrback(trapIgnored)
return d
def setReceived(self, request):
"""
Called when a roster push for a new or update item was received.
@param request: The push request.
@type request: L{RosterRequest}
"""
if hasattr(self, 'onRosterSet'):
warnings.warn(
"wokkel.xmppim.RosterClientProtocol.onRosterSet "
"was deprecated in Wokkel 0.7.1; "
"please use RosterClientProtocol.setReceived instead.",
DeprecationWarning)
return defer.maybeDeferred(self.onRosterSet, request.item)
def removeReceived(self, request):
"""
Called when a roster push for the removal of an item was received.
@param request: The push request.
@type request: L{RosterRequest}
"""
if hasattr(self, 'onRosterRemove'):
warnings.warn(
"wokkel.xmppim.RosterClientProtocol.onRosterRemove "
"was deprecated in Wokkel 0.7.1; "
"please use RosterClientProtocol.removeReceived instead.",
DeprecationWarning)
return defer.maybeDeferred(self.onRosterRemove,
request.item.entity)
class Message(Stanza):
"""
A message stanza.
"""
stanzaKind = 'message'
childParsers = {
(None, 'body'): '_childParser_body',
(None, 'subject'): '_childParser_subject',
}
def __init__(self, recipient=None, sender=None, body=None, subject=None):
Stanza.__init__(self, recipient, sender)
self.body = body
self.subject = subject
def _childParser_body(self, element):
self.body = unicode(element)
def _childParser_subject(self, element):
self.subject = unicode(element)
def toElement(self):
element = Stanza.toElement(self)
if self.body:
element.addElement('body', content=self.body)
if self.subject:
element.addElement('subject', content=self.subject)
return element
class MessageProtocol(XMPPHandler):
"""
Generic XMPP subprotocol handler for incoming message stanzas.
"""
messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
def connectionInitialized(self):
self.xmlstream.addObserver("/message", self._onMessage)
def _onMessage(self, message):
if message.handled:
return
messageType = message.getAttribute("type")
if messageType == 'error':
return
if messageType not in self.messageTypes:
message["type"] = 'normal'
self.onMessage(message)
def onMessage(self, message):
"""
Called when a message stanza was received.
"""
wokkel-0.7.1/wokkel/data_form.py 0000775 0001750 0001750 00000052415 11707274572 017435 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_data_form -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
Data Forms.
Support for Data Forms as described in
U{XEP-0004}, along with support
for Field Standardization for Data Forms as described in
U{XEP-0068}.
"""
from zope.interface import implements
from zope.interface.common import mapping
from twisted.words.protocols.jabber.jid import JID
from twisted.words.xish import domish
NS_X_DATA = 'jabber:x:data'
class Error(Exception):
"""
Data Forms error.
"""
class FieldNameRequiredError(Error):
"""
A field name is required for this field type.
"""
class TooManyValuesError(Error):
"""
This field is single-value.
"""
class Option(object):
"""
Data Forms field option.
@ivar value: Value of this option.
@type value: C{unicode}
@ivar label: Optional label for this option.
@type label: C{unicode} or C{NoneType}
"""
def __init__(self, value, label=None):
self.value = value
self.label = label
def __repr__(self):
r = ["Option(", repr(self.value)]
if self.label:
r.append(", ")
r.append(repr(self.label))
r.append(")")
return u"".join(r)
def toElement(self):
"""
Return the DOM representation of this option.
@rtype: L{domish.Element}.
"""
option = domish.Element((NS_X_DATA, 'option'))
option.addElement('value', content=self.value)
if self.label:
option['label'] = self.label
return option
@staticmethod
def fromElement(element):
valueElements = list(domish.generateElementsQNamed(element.children,
'value', NS_X_DATA))
if not valueElements:
raise Error("Option has no value")
label = element.getAttribute('label')
return Option(unicode(valueElements[0]), label)
class Field(object):
"""
Data Forms field.
@ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'},
C{'hidden'}, C{'jid-multi'}, C{'jid-single'},
C{'list-multi'}, C{'list-single'}, C{'text-multi'},
C{'text-private'}, C{'text-single'}.
The default is C{'text-single'}.
@type fieldType: C{str}
@ivar var: Field name. Optional if C{fieldType} is C{'fixed'}.
@type var: C{str}
@ivar label: Human readable label for this field.
@type label: C{unicode}
@ivar values: The values for this field, for multi-valued field
types, as a list of C{bool}, C{unicode} or L{JID}.
@type values: C{list}
@ivar options: List of possible values to choose from in a response
to this form as a list of L{Option}s.
@type options: C{list}
@ivar desc: Human readable description for this field.
@type desc: C{unicode}
@ivar required: Whether the field is required to be provided in a
response to this form.
@type required: C{bool}
"""
def __init__(self, fieldType='text-single', var=None, value=None,
values=None, options=None, label=None, desc=None,
required=False):
"""
Initialize this field.
See the identically named instance variables for descriptions.
If C{value} is not C{None}, it overrides C{values}, setting the
given value as the only value for this field.
"""
self.fieldType = fieldType
self.var = var
if value is not None:
self.value = value
else:
self.values = values or []
self.label = label
try:
self.options = [Option(value, label)
for value, label in options.iteritems()]
except AttributeError:
self.options = options or []
self.desc = desc
self.required = required
def __repr__(self):
r = ["Field(fieldType=", repr(self.fieldType)]
if self.var:
r.append(", var=")
r.append(repr(self.var))
if self.label:
r.append(", label=")
r.append(repr(self.label))
if self.desc:
r.append(", desc=")
r.append(repr(self.desc))
if self.required:
r.append(", required=")
r.append(repr(self.required))
if self.values:
r.append(", values=")
r.append(repr(self.values))
if self.options:
r.append(", options=")
r.append(repr(self.options))
r.append(")")
return u"".join(r)
def __value_set(self, value):
"""
Setter of value property.
Sets C{value} as the only element of L{values}.
@type value: C{bool}, C{unicode} or L{JID}
"""
self.values = [value]
def __value_get(self):
"""
Getter of value property.
Returns the first element of L{values}, if present, or C{None}.
"""
if self.values:
return self.values[0]
else:
return None
value = property(__value_get, __value_set, doc="""
The value for this field, for single-valued field types.
This is a special property accessing L{values}. Writing to this
property empties L{values} and then sets the given value as the
only element of L{values}. Reading from this propery returns the
first element of L{values}.
""")
def typeCheck(self):
"""
Check field properties agains the set field type.
"""
if self.var is None and self.fieldType != 'fixed':
raise FieldNameRequiredError()
if self.values:
if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
'text-multi', None) and
len(self.values) > 1):
raise TooManyValuesError()
newValues = []
for value in self.values:
if self.fieldType == 'boolean':
if isinstance(value, (str, unicode)):
checkValue = value.lower()
if not checkValue in ('0', '1', 'false', 'true'):
raise ValueError("Not a boolean")
value = checkValue in ('1', 'true')
value = bool(value)
elif self.fieldType in ('jid-single', 'jid-multi'):
if not hasattr(value, 'full'):
value = JID(value)
newValues.append(value)
self.values = newValues
def toElement(self, asForm=False):
"""
Return the DOM representation of this Field.
@rtype: L{domish.Element}.
"""
self.typeCheck()
field = domish.Element((NS_X_DATA, 'field'))
if self.fieldType:
field['type'] = self.fieldType
if self.var is not None:
field['var'] = self.var
for value in self.values:
if isinstance(value, bool):
value = unicode(value).lower()
else:
value = unicode(value)
field.addElement('value', content=value)
if asForm:
if self.fieldType in ('list-single', 'list-multi'):
for option in self.options:
field.addChild(option.toElement())
if self.label is not None:
field['label'] = self.label
if self.desc is not None:
field.addElement('desc', content=self.desc)
if self.required:
field.addElement('required')
return field
@staticmethod
def _parse_desc(field, element):
desc = unicode(element)
if desc:
field.desc = desc
@staticmethod
def _parse_option(field, element):
field.options.append(Option.fromElement(element))
@staticmethod
def _parse_required(field, element):
field.required = True
@staticmethod
def _parse_value(field, element):
value = unicode(element)
field.values.append(value)
@staticmethod
def fromElement(element):
field = Field(None)
for eAttr, fAttr in {'type': 'fieldType',
'var': 'var',
'label': 'label'}.iteritems():
value = element.getAttribute(eAttr)
if value:
setattr(field, fAttr, value)
for child in element.elements():
if child.uri != NS_X_DATA:
continue
func = getattr(Field, '_parse_' + child.name, None)
if func:
func(field, child)
return field
@staticmethod
def fromDict(fieldDict):
"""
Create a field from a dictionary.
This is a short hand for passing arguments directly on Field object
creation. The field type is represented by the C{'type'} key. For
C{'options'} the value is not a list of L{Option}s, but a dictionary
keyed by value, with an optional label as value.
"""
kwargs = fieldDict.copy()
if 'type' in fieldDict:
kwargs['fieldType'] = fieldDict['type']
del kwargs['type']
if 'options' in fieldDict:
options = []
for value, label in fieldDict['options'].iteritems():
options.append(Option(value, label))
kwargs['options'] = options
return Field(**kwargs)
class Form(object):
"""
Data Form.
There are two similarly named properties of forms. The C{formType} is the
the so-called type of the form, and is set as the C{'type'} attribute
on the form's root element.
The Field Standardization specification in XEP-0068, defines a way to
provide a context for the field names used in this form, by setting a
special hidden field named C{'FORM_TYPE'}, to put the names of all
other fields in the namespace of the value of that field. This namespace
is recorded in the C{formNamespace} instance variable.
A L{Form} also acts as read-only dictionary, with the values of fields
keyed by their name. See L{__getitem__}.
@ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
or {'result'}.
@type formType: C{str}
@ivar title: Natural language title of the form.
@type title: C{unicode}
@ivar instructions: Natural language instructions as a list of C{unicode}
strings without line breaks.
@type instructions: C{list}
@ivar formNamespace: The optional namespace of the field names for this
form. This goes in the special field named C{'FORM_TYPE'}, if set.
@type formNamespace: C{str}
@ivar fields: Dictionary of named fields. Note that this is meant to be
used for reading, only. One should use L{addField} or L{makeFields} and
L{removeField} for adding and removing fields.
@type fields: C{dict}
@ivar fieldList: List of all fields, in the order they are added. Like
C{fields}, this is meant to be used for reading, only.
@type fieldList: C{list}
"""
implements(mapping.IIterableMapping,
mapping.IEnumerableMapping,
mapping.IReadMapping,
mapping.IItemMapping)
def __init__(self, formType, title=None, instructions=None,
formNamespace=None, fields=None):
self.formType = formType
self.title = title
self.instructions = instructions or []
self.formNamespace = formNamespace
self.fieldList = []
self.fields = {}
if fields:
for field in fields:
self.addField(field)
def __repr__(self):
r = ["Form(formType=", repr(self.formType)]
if self.title:
r.append(", title=")
r.append(repr(self.title))
if self.instructions:
r.append(", instructions=")
r.append(repr(self.instructions))
if self.formNamespace:
r.append(", formNamespace=")
r.append(repr(self.formNamespace))
if self.fieldList:
r.append(", fields=")
r.append(repr(self.fieldList))
r.append(")")
return u"".join(r)
def addField(self, field):
"""
Add a field to this form.
Fields are added in order, and C{fields} is a dictionary of the
named fields, that is kept in sync only if this method is used for
adding new fields. Multiple fields with the same name are disallowed.
"""
if field.var is not None:
if field.var in self.fields:
raise Error("Duplicate field %r" % field.var)
self.fields[field.var] = field
self.fieldList.append(field)
def removeField(self, field):
"""
Remove a field from this form.
"""
self.fieldList.remove(field)
if field.var is not None:
del self.fields[field.var]
def makeFields(self, values, fieldDefs=None, filterUnknown=True):
"""
Create fields from values and add them to this form.
This creates fields from a mapping of name to value(s) and adds them to
this form. It is typically used for generating outgoing forms.
If C{fieldDefs} is not C{None}, this is used to fill in
additional properties of fields, like the field types, labels and
possible options.
If C{filterUnknown} is C{True} and C{fieldDefs} is not C{None}, fields
will only be created from C{values} with a corresponding entry in
C{fieldDefs}.
If the field type is unknown, the field type is C{None}. When the form
is rendered using L{toElement}, these fields will have no C{'type'}
attribute, and it is up to the receiving party to interpret the values
properly (e.g. by knowing about the FORM_TYPE in C{formNamespace} and
the field name).
@param values: Values to create fields from.
@type values: C{dict}
@param fieldDefs: Field definitions as a dictionary. See
L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
@type fieldDefs: C{dict}
@param filterUnknown: If C{True}, ignore fields that are not in
C{fieldDefs}.
@type filterUnknown: C{bool}
"""
for name, value in values.iteritems():
fieldDict = {'var': name,
'type': None}
if fieldDefs is not None:
if name in fieldDefs:
fieldDict.update(fieldDefs[name])
elif filterUnknown:
continue
if isinstance(value, list):
fieldDict['values'] = value
else:
fieldDict['value'] = value
self.addField(Field.fromDict(fieldDict))
def toElement(self):
"""
Return the DOM representation of this Form.
@rtype: L{domish.Element}
"""
form = domish.Element((NS_X_DATA, 'x'))
form['type'] = self.formType
if self.title:
form.addElement('title', content=self.title)
for instruction in self.instructions:
form.addElement('instructions', content=instruction)
if self.formNamespace is not None:
field = Field('hidden', 'FORM_TYPE', self.formNamespace)
form.addChild(field.toElement())
for field in self.fieldList:
form.addChild(field.toElement(self.formType=='form'))
return form
@staticmethod
def _parse_title(form, element):
title = unicode(element)
if title:
form.title = title
@staticmethod
def _parse_instructions(form, element):
instructions = unicode(element)
if instructions:
form.instructions.append(instructions)
@staticmethod
def _parse_field(form, element):
field = Field.fromElement(element)
if (field.var == "FORM_TYPE" and
field.fieldType == 'hidden' and
field.value):
form.formNamespace = field.value
else:
form.addField(field)
@staticmethod
def fromElement(element):
if (element.uri, element.name) != ((NS_X_DATA, 'x')):
raise Error("Element provided is not a Data Form")
form = Form(element.getAttribute("type"))
for child in element.elements():
if child.uri != NS_X_DATA:
continue
func = getattr(Form, '_parse_' + child.name, None)
if func:
func(form, child)
return form
def __iter__(self):
return iter(self.fields)
def __len__(self):
return len(self.fields)
def __getitem__(self, key):
"""
Called to implement evaluation of self[key].
This returns the value of the field with the name in C{key}. For
multi-value fields, the value is a list, otherwise a single value.
If a field has no type, and the field has multiple values, the value
of the list of values. Otherwise, it will be a single value.
Raises C{KeyError} if there is no field with the name in C{key}.
"""
field = self.fields[key]
if (field.fieldType in ('jid-multi', 'list-multi', 'text-multi') or
(field.fieldType is None and len(field.values) > 1)):
value = field.values
else:
value = field.value
return value
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def __contains__(self, key):
return key in self.fields
def iterkeys(self):
return iter(self)
def itervalues(self):
for key in self:
yield self[key]
def iteritems(self):
for key in self:
yield (key, self[key])
def keys(self):
return list(self)
def values(self):
return list(self.itervalues())
def items(self):
return list(self.iteritems())
def getValues(self):
"""
Extract values from the named form fields.
For all named fields, the corresponding value or values are
returned in a dictionary keyed by the field name. This is equivalent
do C{dict(f)}, where C{f} is a L{Form}.
@see: L{__getitem__}
@rtype: C{dict}
"""
return dict(self)
def typeCheck(self, fieldDefs=None, filterUnknown=False):
"""
Check values of fields according to the field definition.
This method walks all named fields to check their values against their
type, and is typically used for forms received from other entities. The
field definition in C{fieldDefs} is used to check the field type.
If C{filterUnknown} is C{True}, fields that are not present in
C{fieldDefs} are removed from the form.
If the field type is C{None} (when not set by the sending entity),
the type from the field definitition is used, or C{'text-single'} if
that is not set.
If C{fieldDefs} is None, an empty dictionary is assumed. This is
useful for coercing boolean and JID values on forms with type
C{'form'}.
@param fieldDefs: Field definitions as a dictionary. See
L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
@type fieldDefs: C{dict}
@param filterUnknown: If C{True}, remove fields that are not in
C{fieldDefs}.
@type filterUnknown: C{bool}
"""
if fieldDefs is None:
fieldDefs = {}
filtered = []
for name, field in self.fields.iteritems():
if name in fieldDefs:
fieldDef = fieldDefs[name]
if 'type' not in fieldDef:
fieldDef['type'] = 'text-single'
if field.fieldType is None:
field.fieldType = fieldDef['type']
elif field.fieldType != fieldDef['type']:
raise TypeError("Field type for %r is %r, expected %r" %
(name,
field.fieldType,
fieldDef['type']))
else:
# Field type is correct
pass
field.typeCheck()
elif filterUnknown:
filtered.append(field)
elif field.fieldType is not None:
field.typeCheck()
else:
# Unknown field without type, no checking, no filtering
pass
for field in filtered:
self.removeField(field)
def findForm(element, formNamespace):
"""
Find a Data Form.
Look for an element that represents a Data Form with the specified
form namespace as a child element of the given element.
"""
if not element:
return None
for child in element.elements():
if (child.uri, child.name) == ((NS_X_DATA, 'x')):
form = Form.fromElement(child)
if (form.formNamespace == formNamespace or
not form.formNamespace and form.formType=='cancel'):
return form
return None
wokkel-0.7.1/wokkel/disco.py 0000775 0001750 0001750 00000042417 11734574124 016577 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_disco -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Service Discovery.
The XMPP service discovery protocol is documented in
U{XEP-0030}.
"""
from twisted.internet import defer
from twisted.words.protocols.jabber import error, jid
from twisted.words.xish import domish
from wokkel import data_form, generic
from wokkel.iwokkel import IDisco
from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
NS_DISCO = 'http://jabber.org/protocol/disco'
NS_DISCO_INFO = NS_DISCO + '#info'
NS_DISCO_ITEMS = NS_DISCO + '#items'
IQ_GET = '/iq[@type="get"]'
DISCO_INFO = IQ_GET + '/query[@xmlns="' + NS_DISCO_INFO + '"]'
DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + NS_DISCO_ITEMS + '"]'
class DiscoFeature(unicode):
"""
XMPP service discovery feature.
This extends C{unicode} to convert to and from L{domish.Element}, but
further behaves identically.
"""
def toElement(self):
"""
Render to a DOM representation.
@rtype: L{domish.Element}.
"""
element = domish.Element((NS_DISCO_INFO, 'feature'))
element['var'] = unicode(self)
return element
@staticmethod
def fromElement(element):
"""
Parse a DOM representation into a L{DiscoFeature} instance.
@param element: Element that represents the disco feature.
@type element: L{domish.Element}.
@rtype L{DiscoFeature}.
"""
featureURI = element.getAttribute('var', u'')
feature = DiscoFeature(featureURI)
return feature
class DiscoIdentity(object):
"""
XMPP service discovery identity.
@ivar category: The identity category.
@type category: C{unicode}
@ivar type: The identity type.
@type type: C{unicode}
@ivar name: The optional natural language name for this entity.
@type name: C{unicode}
"""
def __init__(self, category, idType, name=None):
self.category = category
self.type = idType
self.name = name
def toElement(self):
"""
Generate a DOM representation.
@rtype: L{domish.Element}.
"""
element = domish.Element((NS_DISCO_INFO, 'identity'))
if self.category:
element['category'] = self.category
if self.type:
element['type'] = self.type
if self.name:
element['name'] = self.name
return element
@staticmethod
def fromElement(element):
"""
Parse a DOM representation into a L{DiscoIdentity} instance.
@param element: Element that represents the disco identity.
@type element: L{domish.Element}.
@rtype L{DiscoIdentity}.
"""
category = element.getAttribute('category')
idType = element.getAttribute('type')
name = element.getAttribute('name')
feature = DiscoIdentity(category, idType, name)
return feature
class DiscoInfo(object):
"""
XMPP service discovery info.
@ivar nodeIdentifier: The optional node this info applies to.
@type nodeIdentifier: C{unicode}
@ivar features: Features as L{DiscoFeature}.
@type features: C{set}
@ivar identities: Identities as a mapping from (category, type) to name,
all C{unicode}.
@type identities: C{dict}
@ivar extensions: Service discovery extensions as a mapping from the
extension form's C{FORM_TYPE} (C{unicode}) to
L{data_form.Form}. Forms with no C{FORM_TYPE} field
are mapped as C{None}. Note that multiple forms
with the same C{FORM_TYPE} have the last in sequence
prevail.
@type extensions: C{dict}
@ivar _items: Sequence of added items.
@type _items: C{list}
"""
def __init__(self):
self.nodeIdentifier = ''
self.features = set()
self.identities = {}
self.extensions = {}
self._items = []
def __iter__(self):
"""
Iterator over sequence of items in the order added.
"""
return iter(self._items)
def append(self, item):
"""
Add a piece of service discovery info.
@param item: A feature, identity or extension form.
@type item: L{DiscoFeature}, L{DiscoIdentity} or L{data_form.Form}
"""
self._items.append(item)
if isinstance(item, DiscoFeature):
self.features.add(item)
elif isinstance(item, DiscoIdentity):
self.identities[(item.category, item.type)] = item.name
elif isinstance(item, data_form.Form):
self.extensions[item.formNamespace] = item
def toElement(self):
"""
Generate a DOM representation.
This takes the items added with C{append} to create a DOM
representation of service discovery information.
@rtype: L{domish.Element}.
"""
element = domish.Element((NS_DISCO_INFO, 'query'))
if self.nodeIdentifier:
element['node'] = self.nodeIdentifier
for item in self:
element.addChild(item.toElement())
return element
@staticmethod
def fromElement(element):
"""
Parse a DOM representation into a L{DiscoInfo} instance.
@param element: Element that represents the disco info.
@type element: L{domish.Element}.
@rtype L{DiscoInfo}.
"""
info = DiscoInfo()
info.nodeIdentifier = element.getAttribute('node', '')
for child in element.elements():
item = None
if (child.uri, child.name) == (NS_DISCO_INFO, 'feature'):
item = DiscoFeature.fromElement(child)
elif (child.uri, child.name) == (NS_DISCO_INFO, 'identity'):
item = DiscoIdentity.fromElement(child)
elif (child.uri, child.name) == (data_form.NS_X_DATA, 'x'):
item = data_form.Form.fromElement(child)
if item is not None:
info.append(item)
return info
class DiscoItem(object):
"""
XMPP service discovery item.
@ivar entity: The entity holding the item.
@type entity: L{jid.JID}
@ivar nodeIdentifier: The optional node identifier for the item.
@type nodeIdentifier: C{unicode}
@ivar name: The optional natural language name for this entity.
@type name: C{unicode}
"""
def __init__(self, entity, nodeIdentifier='', name=None):
self.entity = entity
self.nodeIdentifier = nodeIdentifier
self.name = name
def toElement(self):
"""
Generate a DOM representation.
@rtype: L{domish.Element}.
"""
element = domish.Element((NS_DISCO_ITEMS, 'item'))
if self.entity:
element['jid'] = self.entity.full()
if self.nodeIdentifier:
element['node'] = self.nodeIdentifier
if self.name:
element['name'] = self.name
return element
@staticmethod
def fromElement(element):
"""
Parse a DOM representation into a L{DiscoItem} instance.
@param element: Element that represents the disco iitem.
@type element: L{domish.Element}.
@rtype L{DiscoItem}.
"""
try:
entity = jid.JID(element.getAttribute('jid', ' '))
except jid.InvalidFormat:
entity = None
nodeIdentifier = element.getAttribute('node', '')
name = element.getAttribute('name')
feature = DiscoItem(entity, nodeIdentifier, name)
return feature
class DiscoItems(object):
"""
XMPP service discovery items.
@ivar nodeIdentifier: The optional node this info applies to.
@type nodeIdentifier: C{unicode}
@ivar _items: Sequence of added items.
@type _items: C{list}
"""
def __init__(self):
self.nodeIdentifier = ''
self._items = []
def __iter__(self):
"""
Iterator over sequence of items in the order added.
"""
return iter(self._items)
def append(self, item):
"""
Append item to the sequence of items.
@param item: Item to be added.
@type item: L{DiscoItem}
"""
self._items.append(item)
def toElement(self):
"""
Generate a DOM representation.
This takes the items added with C{append} to create a DOM
representation of service discovery items.
@rtype: L{domish.Element}.
"""
element = domish.Element((NS_DISCO_ITEMS, 'query'))
if self.nodeIdentifier:
element['node'] = self.nodeIdentifier
for item in self:
element.addChild(item.toElement())
return element
@staticmethod
def fromElement(element):
"""
Parse a DOM representation into a L{DiscoItems} instance.
@param element: Element that represents the disco items.
@type element: L{domish.Element}.
@rtype L{DiscoItems}.
"""
info = DiscoItems()
info.nodeIdentifier = element.getAttribute('node', '')
for child in element.elements():
if (child.uri, child.name) == (NS_DISCO_ITEMS, 'item'):
item = DiscoItem.fromElement(child)
info.append(item)
return info
class _DiscoRequest(generic.Request):
"""
A Service Discovery request.
@ivar verb: Type of request: C{'info'} or C{'items'}.
@type verb: C{str}
@ivar nodeIdentifier: Optional node to request info for.
@type nodeIdentifier: C{unicode}
"""
verb = None
nodeIdentifier = ''
_requestVerbMap = {
NS_DISCO_INFO: 'info',
NS_DISCO_ITEMS: 'items',
}
_verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
def __init__(self, verb=None, nodeIdentifier='',
recipient=None, sender=None):
generic.Request.__init__(self, recipient=recipient, sender=sender,
stanzaType='get')
self.verb = verb
self.nodeIdentifier = nodeIdentifier
def parseElement(self, element):
generic.Request.parseElement(self, element)
verbElement = None
for child in element.elements():
if child.name == 'query' and child.uri in self._requestVerbMap:
self.verb = self._requestVerbMap[child.uri]
verbElement = child
if verbElement:
self.nodeIdentifier = verbElement.getAttribute('node', '')
def toElement(self):
element = generic.Request.toElement(self)
childURI = self._verbRequestMap[self.verb]
query = element.addElement((childURI, 'query'))
if self.nodeIdentifier:
query['node'] = self.nodeIdentifier
return element
class DiscoClientProtocol(XMPPHandler):
"""
XMPP Service Discovery client protocol.
"""
def requestInfo(self, entity, nodeIdentifier='', sender=None):
"""
Request information discovery from a node.
@param entity: Entity to send the request to.
@type entity: L{jid.JID}
@param nodeIdentifier: Optional node to request info from.
@type nodeIdentifier: C{unicode}
@param sender: Optional sender address.
@type sender: L{jid.JID}
"""
request = _DiscoRequest('info', nodeIdentifier)
request.sender = sender
request.recipient = entity
d = self.request(request)
d.addCallback(lambda iq: DiscoInfo.fromElement(iq.query))
return d
def requestItems(self, entity, nodeIdentifier='', sender=None):
"""
Request items discovery from a node.
@param entity: Entity to send the request to.
@type entity: L{jid.JID}
@param nodeIdentifier: Optional node to request info from.
@type nodeIdentifier: C{unicode}
@param sender: Optional sender address.
@type sender: L{jid.JID}
"""
request = _DiscoRequest('items', nodeIdentifier)
request.sender = sender
request.recipient = entity
d = self.request(request)
d.addCallback(lambda iq: DiscoItems.fromElement(iq.query))
return d
class DiscoHandler(XMPPHandler, IQHandlerMixin):
"""
Protocol implementation for XMPP Service Discovery.
This handler will listen to XMPP service discovery requests and query the
other handlers in C{parent} (see
L{twisted.words.protocols.jabber.xmlstream.XMPPHandlerCollection})
for their identities, features and items according to L{IDisco}.
"""
iqHandlers = {DISCO_INFO: '_onDiscoInfo',
DISCO_ITEMS: '_onDiscoItems'}
def connectionInitialized(self):
self.xmlstream.addObserver(DISCO_INFO, self.handleRequest)
self.xmlstream.addObserver(DISCO_ITEMS, self.handleRequest)
def _onDiscoInfo(self, iq):
"""
Called for incoming disco info requests.
@param iq: The request iq element.
@type iq: L{Element}
"""
request = _DiscoRequest.fromElement(iq)
def toResponse(info):
if request.nodeIdentifier and not info:
raise error.StanzaError('item-not-found')
else:
response = DiscoInfo()
response.nodeIdentifier = request.nodeIdentifier
for item in info:
response.append(item)
return response.toElement()
d = self.info(request.sender, request.recipient,
request.nodeIdentifier)
d.addCallback(toResponse)
return d
def _onDiscoItems(self, iq):
"""
Called for incoming disco items requests.
@param iq: The request iq element.
@type iq: L{Element}
"""
request = _DiscoRequest.fromElement(iq)
def toResponse(items):
response = DiscoItems()
response.nodeIdentifier = request.nodeIdentifier
for item in items:
response.append(item)
return response.toElement()
d = self.items(request.sender, request.recipient,
request.nodeIdentifier)
d.addCallback(toResponse)
return d
def _gatherResults(self, deferredList):
"""
Gather results from a list of deferreds.
Similar to L{defer.gatherResults}, but flattens the returned results,
consumes errors after the first one and fires the errback of the
returned deferred with the failure of the first deferred that fires its
errback.
@param deferredList: List of deferreds for which the results should be
gathered.
@type deferredList: C{list}
@return: Deferred that fires with a list of gathered results.
@rtype: L{defer.Deferred}
"""
def cb(resultList):
results = []
for success, value in resultList:
results.extend(value)
return results
def eb(failure):
failure.trap(defer.FirstError)
return failure.value.subFailure
d = defer.DeferredList(deferredList, fireOnOneErrback=1,
consumeErrors=1)
d.addCallbacks(cb, eb)
return d
def info(self, requestor, target, nodeIdentifier):
"""
Inspect all sibling protocol handlers for disco info.
Calls the L{getDiscoInfo} method on all child
handlers of the parent, that provide L{IDisco}.
@param requestor: The entity that sent the request.
@type requestor: L{JID}
@param target: The entity the request was sent to.
@type target: L{JID}
@param nodeIdentifier: The optional node being queried, or C{''}.
@type nodeIdentifier: C{unicode}
@return: Deferred with the gathered results from sibling handlers.
@rtype: L{defer.Deferred}
"""
dl = [defer.maybeDeferred(handler.getDiscoInfo, requestor, target,
nodeIdentifier)
for handler in self.parent
if IDisco.providedBy(handler)]
return self._gatherResults(dl)
def items(self, requestor, target, nodeIdentifier):
"""
Inspect all sibling protocol handlers for disco items.
Calls the L{getDiscoItems} method on all child
handlers of the parent, that provide L{IDisco}.
@param requestor: The entity that sent the request.
@type requestor: L{JID}
@param target: The entity the request was sent to.
@type target: L{JID}
@param nodeIdentifier: The optional node being queried, or C{''}.
@type nodeIdentifier: C{unicode}
@return: Deferred with the gathered results from sibling handlers.
@rtype: L{defer.Deferred}
"""
dl = [defer.maybeDeferred(handler.getDiscoItems, requestor, target,
nodeIdentifier)
for handler in self.parent
if IDisco.providedBy(handler)]
return self._gatherResults(dl)
wokkel-0.7.1/wokkel/shim.py 0000775 0001750 0001750 00000002334 11707274572 016434 0 ustar ralphm ralphm 0000000 0000000 # -*- test-case-name: wokkel.test.test_shim -*-
#
# Copyright (c) Ralph Meijer.
# See LICENSE for details.
"""
XMPP Stanza Headers and Internet Metadata.
This protocol is specified in
U{XEP-0131}.
"""
from twisted.words.xish import domish
NS_SHIM = "http://jabber.org/protocol/shim"
class Headers(domish.Element):
def __init__(self, headers):
domish.Element.__init__(self, (NS_SHIM, 'headers'))
for name, value in headers:
self.addElement('header', content=value)['name'] = name
def extractHeaders(stanza):
"""
Extract SHIM headers from stanza.
@param stanza: The stanza to extract headers from.
@type stanza: L{Element}
@return: Headers as a mapping from header name to a list of values.
@rtype: C{dict}
"""
headers = {}
for element in domish.generateElementsQNamed(stanza.children,
'headers', NS_SHIM):
for header in domish.generateElementsQNamed(element.children,
'header', NS_SHIM):
headers.setdefault(header['name'], []).append(unicode(header))
return headers
wokkel-0.7.1/README 0000664 0001750 0001750 00000002174 12074346026 014475 0 ustar ralphm ralphm 0000000 0000000 Wokkel 0.7.1
What is this?
=============
Wokkel is a Python module for experimenting with future enhancements to Twisted
Words, that should eventually be included in the main Twisted main development
tree. Some of the code in Wokkel has already made that transition, but is still
included to be used with older Twisted releases.
Requirements
============
- Python 2.4 or later.
- Twisted 10.0.0 or later.
- python-dateutil
Resources
=========
Wokkel's home is .
Besides the general Twisted resources, help is available on the Twisted-Jabber
mailing list::
Copyright and Warranty
======================
The code in this distribution is Copyright (c) Ralph Meijer, unless
explicitly specified otherwise.
Wokkel is made available under the MIT License. The included LICENSE file
describes this in detail.
Contributors
============
- Christopher Zorn
- Jack Moffitt
- Mike Malone
- Pablo Martín
- Fabio Forno
- Kandaurov Oleg
- Jérôme Poisson
- Ilja Braude
- Alexey Bezhan
- Mayank Singh
Author
======
Ralph Meijer