Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop

This commit is contained in:
Nathan Fritz 2011-08-18 00:35:37 -07:00
commit 4ea22ff69b
15 changed files with 409 additions and 204 deletions

8
docs/api/basexmpp.rst Normal file
View file

@ -0,0 +1,8 @@
========
basexmpp
========
.. module:: sleekxmpp.basexmpp
.. autoclass:: BaseXMPP
:members:

8
docs/api/xmlstream.rst Normal file
View file

@ -0,0 +1,8 @@
=========
xmlstream
=========
.. module:: sleekxmpp.xmlstream
.. autoclass:: XMLStream
:members:

2
docs/features.rst Normal file
View file

@ -0,0 +1,2 @@
How to Use Stream Features
==========================

View file

@ -1,3 +1,5 @@
.. _echobot:
===============================
SleekXMPP Quickstart - Echo Bot
===============================
@ -386,127 +388,3 @@ can also be found in the SleekXMPP `examples directory <http://github.com/fritzy
.. include:: ../../examples/echo_client.py
:literal:
..
.. #!/usr/bin/env python
.. # -*- coding: utf-8 -*-
.. import sys
.. import logging
.. import time
.. import getpass
.. from optparse import OptionParser
..
.. import sleekxmpp
..
..
.. class EchoBot(sleekxmpp.ClientXMPP):
..
.. """
.. A simple SleekXMPP bot that will echo messages it
.. receives, along with a short thank you message.
.. """
..
.. def __init__(self, jid, password):
.. sleekxmpp.ClientXMPP.__init__(self, jid, password)
..
.. # The session_start event will be triggered when
.. # the bot 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 intialize
.. # our roster.
.. 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.
..
.. Typical actions for the session_start event are
.. requesting the roster and broadcasting an intial
.. presence stanza.
..
.. Arguments:
.. event -- An empty dictionary. The session_start
.. event does not provide any additional
.. data.
.. """
.. self.send_presence()
.. self.get_roster()
..
.. 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.
..
.. Arguments:
.. msg -- The received message stanza. See the documentation
.. for stanza objects and the Message stanza to see
.. how it may be used.
.. """
.. if msg['type'] in ('normal', 'chat'):
.. 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")
..
.. opts, args = optp.parse_args()
..
.. # Setup logging.
.. logging.basicConfig(level=opts.loglevel,
.. format='%(levelname)-8s %(message)s')
..
.. if opts.jid is None:
.. opts.jid = raw_input("Username: ")
.. if opts.password is None:
.. opts.password = getpass.getpass("Password: ")
..
.. # Setup the EchoBot and register plugins. Note that while plugins may
.. # have interdependencies, the order in which you register them does
.. # not matter.
.. xmpp = EchoBot(opts.jid, opts.password)
.. xmpp.register_plugin('xep_0030') # Service Discovery
.. xmpp.register_plugin('xep_0199') # XMPP Ping
..
.. # If you are working with an OpenFire server, you may need
.. # to adjust the SSL version used:
.. # xmpp.ssl_version = ssl.PROTOCOL_SSLv3
..
.. # Connect to the XMPP server and start processing XMPP stanzas.
.. if xmpp.connect():
.. # If you do not have the pydns library installed, you will need
.. # to manually specify the name of the server if it does not match
.. # the one in the JID. For example, to use Google Talk you would
.. # need to use:
.. #
.. # if xmpp.connect(('talk.google.com', 5222)):
.. # ...
.. xmpp.process(threaded=False)
.. print("Done")
.. else:
.. print("Unable to connect.")

View file

@ -1,5 +1,94 @@
Login, Send a Message, and Disconnect
=====================================
Sign in, Send a Message, and Disconnect
=======================================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
A common use case for SleekXMPP is to send one-off messages from
time to time.
time to time. For example, one use case could be sending out a notice when
a shell script finishes a task.
We will create our one-shot bot based on the pattern explained in :ref:`echobot`. To
start, we create a client class based on :class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>` and
register a handler for the :term:`session_start` event. We will also accept parameters
for the JID that will receive our message, and the string content of the message.
.. code-block:: python
import sleekxmpp
class SendMsgBot(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, recipient, msg):
super(SendMsgBot, self).__init__(jid, password)
self.recipient = recipient
self.msg = msg
self.add_event_handler('session_start', self.start)
def start(self, event):
self.send_presence()
self.get_roster()
Note that as in :ref:`echobot`, we need to include send an initial presence and request
the roster. Next, we want to send our message, and to do that we will use :meth:`send_message <sleekxmpp.basexmpp.BaseXMPP.send_message>`.
.. code-block:: python
def start(self, event):
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)
Finally, we need to disconnect the client using :meth:`disconnect <sleekxmpp.xmlstream.XMLStream.disconnect>`.
Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call
:meth:`disconnect <sleekxmpp.xmlstream.XMLStream.disconnect>` without any parameters, then it is possible
for the client to disconnect before the send queue is processed and the message is actually
sent on the wire. To ensure that our message is processed, we use
:meth:`disconnect(wait=True) <sleekxmpp.xmlstream.XMLStream.disconnect>`.
.. code-block:: python
def start(self, event):
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)
self.disconnect(wait=True)
.. warning::
If you happen to be adding stanzas to the send queue faster than the send thread
can process them, then :meth:`disconnect(wait=True) <sleekxmpp.xmlstream.XMLStream.disconnect>`
will block and not disconnect.
Final Product
-------------
.. compound::
The final step is to create a small runner script for initialising our ``SendMsgBot`` class and adding some
basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive
at the code below. To experiment with this example, you can use:
.. code-block:: sh
python send_client.py -d -j oneshot@example.com -t someone@example.net -m "This is a message"
which will prompt for the password and then log in, send your message, and then disconnect. To test, open
your regular IM client with the account you wish to send messages to. When you run the ``send_client.py``
example and instruct it to send your IM client account a message, you should receive the message you
gave. If the two JIDs you use also have a mutual presence subscription (they're on each other's buddy lists)
then you will also see the ``SendMsgBot`` client come online and then go offline.
.. include:: ../../examples/send_client.py
:literal:

View file

@ -67,7 +67,7 @@ SleekXMPP's design goals and philosphy are:
Getting Started (with Examples)
-------------------------------
.. toctree::
:maxdepth: 2
:maxdepth: 1
getting_started/echobot
getting_started/sendlogout
@ -82,27 +82,29 @@ Getting Started (with Examples)
Tutorials, FAQs, and How To Guides
----------------------------------
.. toctree::
:maxdepth: 2
:maxdepth: 1
xeps
xmpp_tdg
create_plugin
features
sasl
handlersmatchers
Plugin Guides
~~~~~~~~~~~~~
.. toctree::
:maxdepth: 2
:maxdepth: 1
guide_xep_0030
SleekXMPP Architecture and Design
---------------------------------
.. toctree::
:maxdepth: 2
:maxdepth: 3
architecture.rst
architecture
plugin_arch
API Reference
-------------
@ -111,6 +113,8 @@ API Reference
event_index
api/clientxmpp
api/basexmpp
api/xmlstream
Additional Info
---------------

2
docs/plugin_arch.rst Normal file
View file

@ -0,0 +1,2 @@
Plugin Architecture
===================

171
examples/roster_browser.py Normal file
View file

@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import time
import logging
import getpass
import threading
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.exceptions import IqError, IqTimeout
# 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')
class RosterBrowser(sleekxmpp.ClientXMPP):
"""
A basic script for dumping a client's roster to
the command line.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot 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 intialize
# our roster. We need threaded=True so that the
# session_start handler doesn't block event processing
# while we wait for presence stanzas to arrive.
self.add_event_handler("session_start", self.start, threaded=True)
self.add_event_handler("changed_status", self.wait_for_presences)
self.received = set()
self.presences_received = threading.Event()
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
try:
self.get_roster()
except IqError as err:
print('Error: %' % err.iq['error']['condition'])
except IqTimeout:
print('Error: Request timed out')
self.send_presence()
print('Waiting for presence updates...\n')
self.presences_received.wait(5)
print('Roster for %s' % self.boundjid.bare)
groups = self.client_roster.groups()
for group in groups:
print('\n%s' % group)
print('-' * 72)
for jid in groups[group]:
sub = self.client_roster[jid]['subscription']
name = self.client_roster[jid]['name']
if self.client_roster[jid]['name']:
print(' %s (%s) [%s]' % (name, jid, sub))
else:
print(' %s [%s]' % (jid, sub))
connections = self.client_roster.presence(jid)
for res, pres in connections.items():
show = 'available'
if pres['show']:
show = pres['show']
print(' - %s (%s)' % (res, show))
if pres['status']:
print(' %s' % pres['status'])
self.disconnect()
def wait_for_presences(self, pres):
"""
Track how many roster entries have received presence updates.
"""
self.received.add(pres['from'].bare)
if len(self.received) >= len(self.client_roster.keys()):
self.presences_received.set()
else:
self.presences_received.clear()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
# 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")
opts,args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = RosterBrowser(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
else:
print("Unable to connect.")

View file

@ -29,13 +29,18 @@ if sys.version_info < (3, 0):
class SendMsgBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that will echo messages it
receives, along with a short thank you message.
A basic SleekXMPP bot that will log in, send a message,
and then log out.
"""
def __init__(self, jid, password):
def __init__(self, jid, password, recipient, message):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The message we wish to send, and the JID that
# will receive it.
self.recipient = recipient
self.msg = message
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
@ -43,11 +48,6 @@ class SendMsgBot(sleekxmpp.ClientXMPP):
# our roster.
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.
@ -63,27 +63,14 @@ class SendMsgBot(sleekxmpp.ClientXMPP):
"""
self.send_presence()
self.get_roster()
msg = self.Message()
msg['to'] = 'user@example.com'
msg['type'] = 'chat'
msg['body'] = "Hello there!"
msg.send()
self.disconnect()
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.
self.send_message(mto=self.recipient,
mbody=self.msg,
mtype='chat')
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
#msg.reply("Thanks for sending\n%(body)s" % msg).send()
print "Msg rceived from %(body)s: %(jid)s" % msg
# Using wait=True ensures that the send queue will be
# emptied before ending the session.
self.disconnect(wait=True)
if __name__ == '__main__':
@ -106,6 +93,10 @@ if __name__ == '__main__':
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-t", "--to", dest="to",
help="JID to send the message to")
optp.add_option("-m", "--message", dest="message",
help="message to send")
opts, args = optp.parse_args()
@ -117,14 +108,16 @@ if __name__ == '__main__':
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.to is None:
opts.to = raw_input("Send To: ")
if opts.message is None:
opts.message = raw_input("Message: ")
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = SendMsgBot(opts.jid, opts.password)
xmpp = SendMsgBot(opts.jid, opts.password, opts.to, opts.message)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0060') # PubSub
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you may need

View file

@ -85,7 +85,7 @@ class FormField(ElementBase):
return None
elif self._type == 'boolean':
return valsXML[0].text in self.true_values
elif self._type in self.multi_value_types:
elif self._type in self.multi_value_types or len(valsXML) > 1:
values = []
for valXML in valsXML:
if valXML.text is None:
@ -95,6 +95,8 @@ class FormField(ElementBase):
values = "\n".join(values)
return values
else:
if valsXML[0].text is None:
return ''
return valsXML[0].text
def set_answer(self, answer):

View file

@ -112,7 +112,4 @@ class xep_0012(base.base_plugin):
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq.get('id')
result = iq.send()
if result and result is not None and result.get('type', 'error') != 'error':
return result['last_activity']['seconds']
else:
return False

View file

@ -188,8 +188,12 @@ class xep_0045(base.base_plugin):
iq['from'] = ifrom
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
iq.append(query)
# For now, swallow errors to preserve existing API
try:
result = iq.send()
if result['type'] == 'error':
except IqError:
return False
except IqTimeout:
return False
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if xform is None: return False
@ -209,8 +213,12 @@ class xep_0045(base.base_plugin):
form = form.getXML('submit')
query.append(form)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
result = iq.send()
if result['type'] == 'error':
except IqError:
return False
except IqTimeout:
return False
return True
@ -254,8 +262,12 @@ class xep_0045(base.base_plugin):
destroy.append(xreason)
query.append(destroy)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
r = iq.send()
if r is False or r['type'] == 'error':
except IqError:
return False
except IqTimeout:
return False
return True
@ -271,9 +283,13 @@ class xep_0045(base.base_plugin):
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
# For now, swallow errors to preserve existing API
try:
result = iq.send()
if result is False or result['type'] != 'result':
raise ValueError
except IqError:
return False
except IqTimeout:
return False
return True
def invite(self, room, jid, reason='', mfrom=''):
@ -303,8 +319,12 @@ class xep_0045(base.base_plugin):
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
iq['to'] = room
iq['from'] = ifrom
# For now, swallow errors to preserve existing API
try:
result = iq.send()
if result is None or result['type'] != 'result':
except IqError:
raise ValueError
except IqTimeout:
raise ValueError
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if form is None:

View file

@ -56,11 +56,17 @@ class xep_0078(base_plugin):
iq['type'] = 'get'
iq['to'] = self.xmpp.boundjid.host
iq['auth']['username'] = self.xmpp.boundjid.user
resp = iq.send(now=True)
if resp is None or resp['type'] != 'result':
try:
resp = iq.send(now=True)
except IqError:
log.info("Authentication failed: %s" % resp['error']['condition'])
self.xmpp.event('failed_auth', resp, direct=True)
self.xmpp.event('failed_auth', direct=True)
self.xmpp.disconnect()
return True
except IqTimeout:
log.info("Authentication failed: %s" % 'timeout')
self.xmpp.event('failed_auth', direct=True)
self.xmpp.disconnect()
return True
@ -91,8 +97,17 @@ class xep_0078(base_plugin):
iq['auth']['password'] = self.xmpp.password
# Step 3: Send credentials
try:
result = iq.send(now=True)
if result is not None and result.attrib['type'] == 'result':
except IqError as err:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
except IqTimeout:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
self.xmpp.features.add('auth')
self.xmpp.authenticated = True
@ -100,9 +115,5 @@ class xep_0078(base_plugin):
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()
self.xmpp.event('session_start')
else:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth")
return True

View file

@ -11,6 +11,7 @@ import logging
import sleekxmpp
from sleekxmpp import Iq
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream.handler import Callback
@ -89,8 +90,13 @@ class xep_0199(base_plugin):
def scheduled_ping():
"""Send ping request to the server."""
log.debug("Pinging...")
resp = self.send_ping(self.xmpp.boundjid.host, self.timeout)
if resp is None or resp is False:
try:
self.send_ping(self.xmpp.boundjid.host, self.timeout)
except IqError:
log.debug("Ping response was an error." + \
"Requesting Reconnect.")
self.xmpp.reconnect()
except IqTimeout:
log.debug("Did not recieve ping back in time." + \
"Requesting Reconnect.")
self.xmpp.reconnect()
@ -142,9 +148,14 @@ class xep_0199(base_plugin):
iq.enable('ping')
start_time = time.clock()
try:
resp = iq.send(block=block,
timeout=timeout,
callback=callback)
except IqError as err:
resp = err.iq
end_time = time.clock()
delay = end_time - start_time
@ -152,9 +163,6 @@ class xep_0199(base_plugin):
if not block:
return None
if not resp or resp['type'] == 'error':
return False
log.debug("Pong: %s %f" % (jid, delay))
return delay

View file

@ -75,6 +75,10 @@ class RosterNode(object):
self.add(key, save=True)
return self._jids[key]
def __len__(self):
"""Return the number of JIDs referenced by the roster."""
return len(self._jids)
def keys(self):
"""Return a list of all subscribed JIDs."""
return self._jids.keys()
@ -83,6 +87,16 @@ class RosterNode(object):
"""Returns whether the roster has a JID."""
return jid in self._jids
def groups(self):
"""Return a dictionary mapping group names to JIDs."""
result = {}
for jid in self._jids:
for group in self._jids[jid]['groups']:
if group not in result:
result[group] = []
result[group].append(jid)
return result
def __iter__(self):
"""Iterate over the roster items."""
return self._jids.__iter__()
@ -204,10 +218,8 @@ class RosterNode(object):
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,
'groups': groups}}
response = iq.send(block, timeout, callback)
if response in [False, None] or isinstance(response, Iq):
return response
return response and response['type'] == 'result'
return iq.send(block, timeout, callback)
def presence(self, jid, resource=None):
"""