Compare commits

...

32 Commits

Author SHA1 Message Date
Lance Stout
7945b3e738 Remove the config_component example in favor of echo_component.
The roster portion of the example is too outdated.
2011-11-18 16:26:02 -08:00
Lance Stout
d4c1ff5309 Also fire changed_status when the status text changes for a resource. 2011-11-18 13:57:41 -08:00
Lance Stout
22868c3924 Fix changed_status event
Once again only fires when a resource's presence show value changes.
2011-11-18 13:39:02 -08:00
Lance Stout
2de1be188c Add echo component example. 2011-11-17 12:25:56 -08:00
Lance Stout
9faecec2db Simplify boilerplate example. 2011-11-14 12:00:21 -08:00
Lance Stout
5d7111fe3b Update list of stable releases. 2011-11-14 11:46:07 -08:00
Lance Stout
0c86f8288d No need to continue processing loop if an error ocurred and auto_reconnect=False. 2011-11-14 11:21:05 -08:00
Lance Stout
5a6a65fd9f Fix typo 2011-11-14 11:20:53 -08:00
Lance Stout
43c4d23896 Explicitly test for inequality in JIDs.
Fixes issue #113
2011-11-14 09:15:43 -08:00
Lance Stout
9f9e8db814 Add use_ssl parameter to ClientXMPP.connect 2011-11-11 01:52:18 -08:00
Lance Stout
b8efcc7cf0 Don't just call self.disconnect in self.reconnect.
It messes up the auto_reconnect value and causes the XML processing
loop to spin wildly with errors on a stream disconnect.
2011-11-08 19:23:53 -08:00
Lance Stout
2f29d18e53 Use setuptools if available. 2011-11-08 07:01:16 -08:00
Lance Stout
888e286a09 Continue trying to reconnect, even if the attempt fails.
The transition from disconnected to connected states must be done in a
loop in case the transition fails, not just once and hope it worked.
2011-11-07 01:13:34 -08:00
Lance Stout
1a93a187f0 Fix a crash when removing a contact.
Original author: louiz
2011-11-06 08:33:03 -08:00
Lance Stout
a8d5da5091 Restore original behaviour for auto_authorize and auto_subscribe.
The change to using the new roster broke the original auto_* values
and used per-roster versions. The original auto_* values will now set
the behaviour globally. Use the per-roster values to override for a
specific JID.
2011-11-06 08:25:29 -08:00
Lance Stout
e2720fac9e FIX SCRAM-SHA-1-PLUS
The mechanism name was being correctly de-plussed, but then we used the
original, -PLUS, name to extract the hash, finding SHA-1-PLUS and therefore
finding no match.

Test-Information:

Tested with Sleek against an Isode M-Link with SCRAM-SHA-1-PLUS available.

Author: dwd
2011-10-27 15:16:54 -04:00
Lance Stout
4374729f20 Update the docs for XEP-0060 publish method. 2011-10-11 20:37:50 -04:00
Lance Stout
87999333cb Fix MUC methods to optionally specify the sending JID.
Should fix issue #107
2011-10-10 11:31:03 -04:00
Lance Stout
335dc2927b Break reference cycle to fix potential memory leaks for callback handlers. 2011-10-08 17:31:30 -04:00
Lance Stout
ccbef6b696 Fix typos in the roster update method. 2011-10-07 18:13:50 -04:00
Lance Stout
3e384d3cfe XEP-0009 will likely be updated to use <base64 /> instead of <Base64 />
Both are supported when reading, but <base64 /> will be used for output.
2011-10-05 12:09:50 -04:00
Lance Stout
e33949c397 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-10-04 10:37:42 -04:00
Lance Stout
eccac859ad Fix missing import statement.
Fixes issue #105
2011-10-04 10:36:52 -04:00
Lance Stout
7dd586f2fd Merge pull request #104 from correl/develop
Make RPC events threaded
2011-10-03 13:19:16 -07:00
Correl Roush
3607c5b792 Make RPC events threaded
Allows, for example, an RPC service to make remote RPC calls with its
own connection without blocking its own thread waiting for the result.
2011-10-03 14:32:48 -04:00
Lance Stout
e37adace62 Allow SASL mechanism to be set when creating a ClientXMPP instance.
Instead of using:

    ClientXMPP(jid, password, plugin_config={
        'feature_mechanisms': {'use_mech': 'SOME-MECH'}})

You can use:

    ClientXMPP(jid, password, sasl_mech='SOME-MECH')

If you need to change the mechanism after instantiation, use:

    xmpp['feature_mechanisms'].sasl.mech = 'SCRAM-MD5'
2011-09-28 22:48:30 -04:00
Lance Stout
d10f591bf4 Expand live stream testing capabilities. 2011-09-28 17:26:29 -04:00
Lance Stout
262da78ca7 Fix del_event_handler for Python3 (different semantics for filter()).
Fixes issue #103
2011-09-23 12:03:49 -04:00
Lance Stout
0b83edf439 Fix regression for handling the case where the server terminates the stream.
The processing loop was continuing to call __read_xml after </stream>
was received, which caused SyntaxErrors (can't find starting element).

This should fix issue #102
2011-09-22 01:32:44 -04:00
Nathan Fritz
cf7fcf496e SyntaxError requires a restart 2011-09-19 11:53:09 -07:00
Lance Stout
1765271f84 Make get_node_config block by default. 2011-09-02 11:52:56 -07:00
Lance Stout
0ec79f8dc3 Tweak setup.py, and bump dev version to RC3. 2011-09-01 16:47:30 -07:00
24 changed files with 533 additions and 309 deletions

View File

@@ -45,6 +45,9 @@ The latest source code for SleekXMPP may be found on `Github
``develop`` branch.
**Stable Releases**
- `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
- `1.0 RC2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC2>`_
- `1.0 RC1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC1>`_
- `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
- `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
- `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
@@ -109,13 +112,21 @@ SleekXMPP projects::
def __init__(self, jid, password):
ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
def start(self, event):
self.register_plugin('xep_0030') # Service Discovery
self.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you will
# need to use a different SSL version:
# import ssl
# self.ssl_version = ssl.PROTOCOL_SSLv3
def session_start(self, event):
self.send_presence()
# Most get_* methods from plugins use Iq stanzas, which
# Most get_*/set_* methods from plugins use Iq stanzas, which
# can generate IqError and IqTimeout exceptions
try:
self.get_roster()
@@ -140,19 +151,8 @@ SleekXMPP projects::
format='%(levelname)-8s %(message)s')
xmpp = EchoBot('somejid@example.com', 'use_getpass')
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you will need
# to use a different SSL version:
#
# import ssl
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
if xmpp.connect():
xmpp.process(block=True)
else:
print("Unable to connect.")
xmpp.connect():
xmpp.process(block=True)
Credits

View File

@@ -1,10 +0,0 @@
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>

View File

@@ -1,192 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class Config(ElementBase):
"""
In order to make loading and manipulating an XML config
file easier, we will create a custom stanza object for
our config XML file contents. See the documentation
on stanza objects for more information on how to create
and use stanza objects and stanza plugins.
We will reuse the IQ roster query stanza to store roster
information since it already exists.
Example config XML:
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>
"""
name = "config"
namespace = "sleekxmpp:config"
interfaces = set(('jid', 'secret', 'server', 'port'))
sub_interfaces = interfaces
registerStanzaPlugin(Config, Roster)
class ConfigComponent(ComponentXMPP):
"""
A simple SleekXMPP component that uses an external XML
file to store its configuration data. To make testing
that the component works, it will also echo messages sent
to it.
"""
def __init__(self, config):
"""
Create a ConfigComponent.
Arguments:
config -- The XML contents of the config file.
config_file -- The XML config file object itself.
"""
ComponentXMPP.__init__(self, config['jid'],
config['secret'],
config['server'],
config['port'])
# Store the roster information.
self.roster = config['roster']['items']
# The session_start event will be triggered when
# the component establishes its connection with the
# server and the XML streams are ready for use. We
# want to listen for this event so that we we can
# broadcast any needed initial presence stanzas.
self.add_event_handler("session_start", self.start)
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
"""
Process the session_start event.
The typical action for the session_start event in a component
is to broadcast presence stanzas to all subscribers to the
component. Note that the component does not have a roster
provided by the XMPP server. In this case, we have possibly
saved a roster in the component's configuration file.
Since the component may use any number of JIDs, you should
also include the JID that is sending the presence.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
for jid in self.roster:
if self.roster[jid]['subscription'] != 'none':
self.sendPresence(pfrom=self.jid, pto=jid)
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Since a component may send messages from any number of JIDs,
it is best to always include a from JID.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
# The reply method will use the messages 'to' JID as the
# outgoing reply's 'from' JID.
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# Component name and secret options.
optp.add_option("-c", "--config", help="path to config file",
dest="config", default="config.xml")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
# Load configuration data.
config_file = open(opts.config, 'r+')
config_data = "\n".join([line for line in config_file])
config = Config(xml=ET.fromstring(config_data))
config_file.close()
# Setup the ConfigComponent and register plugins. Note that while plugins
# may have interdependencies, the order in which you register them does
# not matter.
xmpp = ConfigComponent(config)
xmpp.registerPlugin('xep_0030') # Service Discovery
xmpp.registerPlugin('xep_0004') # Data Forms
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

122
examples/echo_component.py Executable file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.componentxmpp import ComponentXMPP
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class EchoComponent(ComponentXMPP):
"""
A simple SleekXMPP component that echoes messages.
"""
def __init__(self, jid, secret, server, port):
ComponentXMPP.__init__(self, jid, secret, server, port)
# You don't need a session_start handler, but that is
# where you would broadcast initial presence.
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Since a component may send messages from any number of JIDs,
it is best to always include a from JID.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
# The reply method will use the messages 'to' JID as the
# outgoing reply's 'from' JID.
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-s", "--server", dest="server",
help="server to connect to")
optp.add_option("-P", "--port", dest="port",
help="port to connect to")
opts, args = optp.parse_args()
if opts.jid is None:
opts.jid = raw_input("Component JID: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.server is None:
opts.server = raw_input("Server: ")
if opts.port is None:
opts.port = int(raw_input("Port: "))
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
# Setup the EchoComponent and register plugins. Note that while plugins
# may have interdependencies, the order in which you register them does
# not matter.
xmpp = EchoComponent(opts.jid, opts.password, opts.server, opts.port)
xmpp.registerPlugin('xep_0030') # Service Discovery
xmpp.registerPlugin('xep_0004') # Data Forms
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

View File

@@ -8,7 +8,10 @@
# file, which you should have received as part of this distribution.
import sys
from distutils.core import setup, Command
try:
from setuptools import setup, Command
except ImportError:
from distutils.core import setup, Command
# from ez_setup import use_setuptools
from testall import TestCommand
@@ -34,10 +37,10 @@ with open('README.rst') as readme:
CLASSIFIERS = [ 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python 2.6',
'Programming Language :: Python 2.7',
'Programming Language :: Python 3.1',
'Programming Language :: Python 3.2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Topic :: Software Development :: Libraries :: Python Modules',
]

View File

@@ -106,9 +106,6 @@ class BaseXMPP(XMLStream):
self.client_roster = self.roster[self.boundjid.bare]
self.is_component = False
self.auto_authorize = True
self.auto_subscribe = True
self.sentpresence = False
self.stanza = sleekxmpp.stanza
@@ -640,6 +637,46 @@ class BaseXMPP(XMLStream):
log.warning("server property deprecated. Use boundjid.host")
self.boundjid.server = value
@property
def auto_authorize(self):
"""
Auto accept or deny subscription requests.
If True, auto accept subscription requests.
If False, auto deny subscription requests.
If None, don't automatically respond.
"""
return self.roster.auto_authorize
@auto_authorize.setter
def auto_authorize(self, value):
"""
Auto accept or deny subscription requests.
If True, auto accept subscription requests.
If False, auto deny subscription requests.
If None, don't automatically respond.
"""
self.roster.auto_authorize = value
@property
def auto_subscribe(self):
"""
Auto send requests for mutual subscriptions.
If True, auto send mutual subscription requests.
"""
return self.roster.auto_subscribe
@auto_subscribe.setter
def auto_subscribe(self, value):
"""
Auto send requests for mutual subscriptions.
If True, auto send mutual subscription requests.
"""
self.roster.auto_subscribe = value
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
log.debug("setting jid to %s" % jid)
@@ -741,8 +778,6 @@ class BaseXMPP(XMLStream):
not presence['type'] in presence.showtypes:
return
self.event("changed_status", presence)
def exception(self, exception):
"""
Process any uncaught exceptions, notably IqError and

View File

@@ -59,7 +59,7 @@ class ClientXMPP(BaseXMPP):
"""
def __init__(self, jid, password, ssl=False, plugin_config={},
plugin_whitelist=[], escape_quotes=True):
plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
"""
Create a new SleekXMPP client.
@@ -114,11 +114,13 @@ class ClientXMPP(BaseXMPP):
# Setup default stream features
self.register_plugin('feature_starttls')
self.register_plugin('feature_mechanisms')
self.register_plugin('feature_bind')
self.register_plugin('feature_session')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
def connect(self, address=tuple(), reattempt=True, use_tls=True):
def connect(self, address=tuple(), reattempt=True,
use_tls=True, use_ssl=False):
"""
Connect to the XMPP server.
@@ -132,13 +134,16 @@ class ClientXMPP(BaseXMPP):
error occurs. Defaults to True.
use_tls -- Indicates if TLS should be used for the
connection. Defaults to True.
use_ssl -- Indicates if the older SSL connection method
should be used. Defaults to False.
"""
self.session_started_event.clear()
if not address:
address = (self.boundjid.host, 5222)
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, reattempt=reattempt)
use_tls=use_tls, use_ssl=use_ssl,
reattempt=reattempt)
def get_dns_records(self, domain, port=None):
"""
@@ -210,7 +215,7 @@ class ClientXMPP(BaseXMPP):
Will be executed when the roster is received.
Implies block=False.
"""
return self.client_roster.updtae(jid, name, subscription, groups,
return self.client_roster.update(jid, name, subscription, groups,
block, timeout, callback)
def del_roster_item(self, jid):

View File

@@ -63,7 +63,7 @@ def _py2xml(*args):
double.text = str(x)
val.append(double)
elif type(x) is rpcbase64:
b64 = ET.Element("Base64")
b64 = ET.Element("base64")
b64.text = x.encoded()
val.append(b64)
elif type(x) is rpctime:
@@ -110,7 +110,10 @@ def _xml2py(value):
return value.find('{%s}string' % namespace).text
if value.find('{%s}double' % namespace) is not None:
return float(value.find('{%s}double' % namespace).text)
if value.find('{%s}base64') is not None:
return rpcbase64(value.find('base64' % namespace).text)
if value.find('{%s}Base64') is not None:
# Older versions of XEP-0009 used Base64
return rpcbase64(value.find('Base64' % namespace).text)
if value.find('{%s}dateTime.iso8601') is not None:
return rpctime(value.find('{%s}dateTime.iso8601'))

View File

@@ -699,10 +699,10 @@ class Remote(object):
with Remote._lock:
del cls._sessions[client.boundjid.bare]
result = RemoteSession(client, _session_close_callback)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error, threaded=True)
if callback is None:
start_event_handler = result._notify
else:

View File

@@ -14,6 +14,7 @@ from .. stanza.presence import Presence
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.matcher.xmlmask import MatchXMLMask
from sleekxmpp.exceptions import IqError, IqTimeout
log = logging.getLogger(__name__)
@@ -222,10 +223,10 @@ class xep_0045(base.base_plugin):
return False
return True
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
""" Join the specified room, requesting 'maxhistory' lines of history.
"""
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
x = ET.Element('{http://jabber.org/protocol/muc}x')
if password:
passelement = ET.Element('password')
@@ -271,7 +272,7 @@ class xep_0045(base.base_plugin):
return False
return True
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
""" Change room affiliation."""
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
raise TypeError
@@ -283,6 +284,7 @@ class xep_0045(base.base_plugin):
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
iq['from'] = ifrom
# For now, swallow errors to preserve existing API
try:
result = iq.send()
@@ -306,13 +308,13 @@ class xep_0045(base.base_plugin):
msg.append(x)
self.xmpp.send(msg)
def leaveMUC(self, room, nick, msg=''):
def leaveMUC(self, room, nick, msg='', pfrom=None):
""" Leave the specified room.
"""
if msg:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
else:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
del self.rooms[room]
def getRoomConfig(self, room, ifrom=''):
@@ -331,12 +333,13 @@ class xep_0045(base.base_plugin):
raise ValueError
return self.xmpp.plugin['xep_0004'].buildForm(form)
def cancelConfig(self, room):
def cancelConfig(self, room, ifrom=None):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
iq['from'] = ifrom
iq.send()
def setRoomConfig(self, room, config, ifrom=''):

View File

@@ -202,7 +202,7 @@ class xep_0060(base_plugin):
iq['pubsub']['options'].append(options)
return iq.send(block=block, callback=callback, timeout=timeout)
def get_node_config(self, jid, node=None, ifrom=None, block=None,
def get_node_config(self, jid, node=None, ifrom=None, block=True,
callback=None, timeout=None):
"""
Retrieve the configuration for a node, or the pubsub service's
@@ -302,11 +302,33 @@ class xep_0060(base_plugin):
def publish(self, jid, node, id=None, payload=None, options=None,
ifrom=None, block=True, callback=None, timeout=None):
"""
Add or edit items in a node.
Add a new item to a node, or edit an existing item.
You may publish an individual item using the item_id and payload
parameters, or you may batch publish by using the items parameter
which accepts a list of id/payload tuples.
For services that support it, you can use the publish command
as an event signal by not including an ID or payload.
When including a payload and you do not provide an ID then
the service will generally create an ID for you.
Publish options may be specified, and how those options
are processed is left to the service, such as treating
the options as preconditions that the node's settings
must match.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to publish the item to.
id -- Optionally specify the ID of the item.
payload -- The item content to publish.
options -- A form of publish options.
ifrom -- Specify the sender's JID.
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
before exiting the send call if blocking is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['publish']['node'] = node

View File

@@ -224,7 +224,7 @@ class RosterItem(object):
if self['to']:
p = self.xmpp.Presence()
p['to'] = self.jid
p['type'] = ['unsubscribe']
p['type'] = 'unsubscribe'
if self.xmpp.is_component:
p['from'] = self.owner
p.send()
@@ -345,7 +345,11 @@ class RosterItem(object):
self.xmpp.event('got_online', presence)
if resource not in self.resources:
self.resources[resource] = {}
old_status = self.resources[resource].get('status', '')
old_show = self.resources[resource].get('show', None)
self.resources[resource].update(data)
if old_show != presence['show'] or old_status != presence['status']:
self.xmpp.event('changed_status', presence)
def handle_unavailable(self, presence):
resource = presence['from'].resource
@@ -353,6 +357,7 @@ class RosterItem(object):
return
if resource in self.resources:
del self.resources[resource]
self.xmpp.event('changed_status', presence)
if not self.resources:
self.xmpp.event('got_offline', presence)

View File

@@ -48,8 +48,8 @@ class Roster(object):
"""
self.xmpp = xmpp
self.db = db
self.auto_authorize = True
self.auto_subscribe = True
self._auto_authorize = True
self._auto_subscribe = True
self._rosters = {}
if self.db:
@@ -138,3 +138,47 @@ class Roster(object):
ppriority=ppriority,
pnick=pnick,
pto=pto)
@property
def auto_authorize(self):
"""
Auto accept or deny subscription requests.
If True, auto accept subscription requests.
If False, auto deny subscription requests.
If None, don't automatically respond.
"""
return self._auto_authorize
@auto_authorize.setter
def auto_authorize(self, value):
"""
Auto accept or deny subscription requests.
If True, auto accept subscription requests.
If False, auto deny subscription requests.
If None, don't automatically respond.
"""
self._auto_authorize = value
for node in self._rosters:
self._rosters[node].auto_authorize = value
@property
def auto_subscribe(self):
"""
Auto send requests for mutual subscriptions.
If True, auto send mutual subscription requests.
"""
return self._auto_subscribe
@auto_subscribe.setter
def auto_subscribe(self, value):
"""
Auto send requests for mutual subscriptions.
If True, auto send mutual subscription requests.
"""
self._auto_subscribe = value
for node in self._rosters:
self._rosters[node].auto_subscribe = value

View File

@@ -209,11 +209,11 @@ class RosterNode(object):
Implies block=False.
"""
self[jid]['name'] = name
self[jid]['groups'] = group
self[jid]['groups'] = groups
self[jid].save()
if not self.xmpp.is_component:
iq = self.Iq()
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,

View File

@@ -293,7 +293,8 @@ class SleekTest(unittest.TestCase):
def stream_start(self, mode='client', skip=True, header=None,
socket='mock', jid='tester@localhost',
password='test', server='localhost',
port=5222, plugins=None):
port=5222, sasl_mech=None,
plugins=None, plugin_config={}):
"""
Initialize an XMPP client or component using a dummy XML stream.
@@ -317,10 +318,13 @@ class SleekTest(unittest.TestCase):
are loaded.
"""
if mode == 'client':
self.xmpp = ClientXMPP(jid, password)
self.xmpp = ClientXMPP(jid, password,
sasl_mech=sasl_mech,
plugin_config=plugin_config)
elif mode == 'component':
self.xmpp = ComponentXMPP(jid, password,
server, port)
server, port,
plugin_config=plugin_config)
else:
raise ValueError("Unknown XMPP connection mode.")
@@ -347,7 +351,10 @@ class SleekTest(unittest.TestCase):
skip_queue.put('started')
self.xmpp.add_event_handler('session_start', wait_for_session)
self.xmpp.connect()
if server is not None:
self.xmpp.connect((server, port))
else:
self.xmpp.connect()
else:
raise ValueError("Unknown socket type.")

View File

@@ -32,7 +32,7 @@ class SCRAM_HMAC(Mechanism):
name = name[:-5]
self._cb = True
self.hash = hash(self.name[6:])
self.hash = hash(name[6:])
if self.hash is None:
raise SASLCancelled(self.sasl, self)
if not self.sasl.tls_active():

View File

@@ -9,5 +9,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
__version__ = '1.0rc2'
__version_info__ = (1, 0, 0, 'rc2', 0)
__version__ = '1.0rc3'
__version_info__ = (1, 0, 0, 'rc3', 0)

View File

@@ -6,6 +6,8 @@
See the file LICENSE for copying permission.
"""
import weakref
class BaseHandler(object):
@@ -43,7 +45,10 @@ class BaseHandler(object):
stream -- The XMLStream instance the handler should monitor.
"""
self.name = name
self.stream = stream
if stream is not None:
self.stream = weakref.ref(stream)
else:
self.stream = None
self._destroy = False
self._payload = None
self._matcher = matcher

View File

@@ -85,14 +85,14 @@ class Waiter(BaseHandler):
value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
"""
if timeout is None:
timeout = self.stream.response_timeout
timeout = self.stream().response_timeout
try:
stanza = self._payload.get(True, timeout)
except queue.Empty:
stanza = False
log.warning("Timed out waiting for %s" % self.name)
self.stream.removeHandler(self.name)
self.stream().remove_handler(self.name)
return stanza
def check_delete(self):

View File

@@ -135,3 +135,9 @@ class JID(object):
"""
other = JID(other)
return self.full == other.full
def __ne__(self, other):
"""
Two JIDs are considered unequal if they are not equal.
"""
return not self == other

View File

@@ -19,6 +19,7 @@ import threading
import time
import types
import random
import weakref
try:
import queue
except ImportError:
@@ -532,9 +533,14 @@ class XMLStream(object):
log.debug("reconnecting...")
self.state.transition('connected', 'disconnected', wait=2.0,
func=self._disconnect, args=(True,))
log.debug("connecting...")
return self.state.transition('disconnected', 'connected',
wait=2.0, func=self._connect)
connected = self.state.transition('disconnected', 'connected',
wait=2.0, func=self._connect)
while not connected:
connected = self.state.transition('disconnected', 'connected',
wait=2.0, func=self._connect)
return connected
def set_socket(self, socket, ignore=False):
"""
@@ -719,7 +725,7 @@ class XMLStream(object):
"""
if handler.stream is None:
self.__handlers.append(handler)
handler.stream = self
handler.stream = weakref.ref(self)
def remove_handler(self, name):
"""
@@ -840,8 +846,9 @@ class XMLStream(object):
def filter_pointers(handler):
return handler[0] != pointer
self.__event_handlers[name] = filter(filter_pointers,
self.__event_handlers[name])
self.__event_handlers[name] = list(filter(
filter_pointers,
self.__event_handlers[name]))
def event_handled(self, name):
"""
@@ -1084,7 +1091,12 @@ class XMLStream(object):
# new connections.
if not self.session_started_event.is_set():
self.send_raw(self.stream_header, now=True)
self.__read_xml()
if not self.__read_xml():
# If the server terminated the stream, end processing
break
except SyntaxError as e:
log.error("Error reading from XML stream.")
self.exception(e)
except KeyboardInterrupt:
log.debug("Keyboard Escape Detected in _process")
self.stop.set()
@@ -1099,11 +1111,8 @@ class XMLStream(object):
if not self.stop.is_set():
log.exception('Connection error.')
if not self.stop.is_set():
if self.auto_reconnect:
self.reconnect()
else:
continue
if not self.stop.is_set() and self.auto_reconnect:
self.reconnect()
else:
self.disconnect()
break
@@ -1115,39 +1124,35 @@ class XMLStream(object):
"""
depth = 0
root = None
try:
for (event, xml) in ET.iterparse(self.filesocket,
(b'end', b'start')):
if event == b'start':
if depth == 0:
# We have received the start of the root element.
root = xml
# Perform any stream initialization actions, such
# as handshakes.
self.stream_end_event.clear()
self.start_stream_handler(root)
depth += 1
if event == b'end':
depth -= 1
if depth == 0:
# The stream's root element has closed,
# terminating the stream.
log.debug("End of stream recieved")
self.stream_end_event.set()
return False
elif depth == 1:
# We only raise events for stanzas that are direct
# children of the root element.
try:
self.__spawn_event(xml)
except RestartStream:
return True
if root:
# Keep the root element empty of children to
# save on memory use.
root.clear()
except SyntaxError:
log.error("Error reading from XML stream.")
for event, xml in ET.iterparse(self.filesocket, (b'end', b'start')):
if event == b'start':
if depth == 0:
# We have received the start of the root element.
root = xml
# Perform any stream initialization actions, such
# as handshakes.
self.stream_end_event.clear()
self.start_stream_handler(root)
depth += 1
if event == b'end':
depth -= 1
if depth == 0:
# The stream's root element has closed,
# terminating the stream.
log.debug("End of stream recieved")
self.stream_end_event.set()
return False
elif depth == 1:
# We only raise events for stanzas that are direct
# children of the root element.
try:
self.__spawn_event(xml)
except RestartStream:
return True
if root is not None:
# Keep the root element empty of children to
# save on memory use.
root.clear()
log.debug("Ending read XML loop")
def _build_stanza(self, xml, default_ns=None):

View File

@@ -48,6 +48,29 @@ class TestEvents(SleekTest):
msg = "Event was not triggered the correct number of times: %s"
self.failUnless(happened == [True], msg % happened)
def testAddDelAddEvent(self):
"""Test adding, then removing, then adding an event handler."""
happened = []
def handletestevent(event):
happened.append(True)
self.xmpp.add_event_handler("test_event", handletestevent)
self.xmpp.event("test_event", {})
self.xmpp.del_event_handler("test_event", handletestevent)
# Should not trigger because it was deleted
self.xmpp.event("test_event", {})
self.xmpp.add_event_handler("test_event", handletestevent)
self.xmpp.event("test_event", {})
# Give the event queue time to process.
time.sleep(0.1)
msg = "Event was not triggered the correct number of times: %s"
self.failUnless(happened == [True, True], msg % happened)
def testDisposableEvent(self):
"""Test disposable handler working, then not being triggered again."""
happened = []

View File

@@ -124,5 +124,18 @@ class TestJIDClass(SleekTest):
'component.someserver',
'component.someserver')
def testJIDEquality(self):
"""Test that JIDs with the same content are equal."""
jid1 = JID('user@domain/resource')
jid2 = JID('user@domain/resource')
self.assertTrue(jid1 == jid2, "Same JIDs are not considered equal")
self.assertFalse(jid1 != jid2, "Same JIDs are considered not equal")
def testJIDInequality(self):
jid1 = JID('user@domain/resource')
jid2 = JID('otheruser@domain/resource')
self.assertFalse(jid1 == jid2, "Same JIDs are not considered equal")
self.assertTrue(jid1 != jid2, "Same JIDs are considered not equal")
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)

View File

@@ -251,5 +251,130 @@ class TestStreamPresence(SleekTest):
self.assertEqual(events, ptypes,
"Not all events raised: %s" % events)
def test_changed_status(self):
"""Test that the changed_status event is handled properly."""
events = []
self.stream_start()
def changed_status(presence):
events.append(presence['type'])
self.xmpp.add_event_handler('changed_status', changed_status)
self.recv("""
<presence from="user@example.com" to="tester@localhost" />
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost" />
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>away</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>away</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>dnd</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>dnd</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>chat</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>chat</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>xa</show>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>xa</show>
</presence>
""")
self.recv("""
<presence from="user@example.com"
to="tester@localhost"
type="unavailable" />
""")
self.recv("""
<presence from="user@example.com"
to="tester@localhost"
type="unavailable" />
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost" />
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost" />
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost" />
""")
# Changed status text, so fire new event
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<status>Testing!</status>
</presence>
""")
# No change in show/status values, no event
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<status>Testing!</status>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>dnd</show>
<status>Testing!</status>
</presence>
""")
self.recv("""
<presence from="user@example.com" to="tester@localhost">
<show>dnd</show>
<status>Testing!</status>
</presence>
""")
time.sleep(0.3)
self.assertEqual(events, ['available', 'away', 'dnd', 'chat',
'xa', 'unavailable', 'available',
'available', 'dnd'],
"Changed status events incorrect: %s" % events)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)