slixmpp-1.2.2/0000755000175000001440000000000013014656513014200 5ustar mathieuiusers00000000000000slixmpp-1.2.2/setup.py0000755000175000001440000000432713004224717015717 0ustar mathieuiusers00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2007-2011 Nathanael C. Fritz # All Rights Reserved # # This software is licensed as described in the README.rst and LICENSE # file, which you should have received as part of this distribution. import os from pathlib import Path from subprocess import call, DEVNULL from tempfile import TemporaryFile try: from setuptools import setup except ImportError: from distutils.core import setup from run_tests import TestCommand from slixmpp.version import __version__ VERSION = __version__ DESCRIPTION = ('Slixmpp is an elegant Python library for XMPP (aka Jabber, ' 'Google Talk, etc).') with open('README.rst', encoding='utf8') as readme: LONG_DESCRIPTION = readme.read() CLASSIFIERS = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3.4', 'Topic :: Software Development :: Libraries :: Python Modules', ] packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')] def check_include(header): command = [os.environ.get('CC', 'cc'), '-E', '-'] with TemporaryFile('w+') as c_file: c_file.write('#include <%s>' % header) c_file.seek(0) try: return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0 except FileNotFoundError: return False ext_modules = None if check_include('stringprep.h'): try: from Cython.Build import cythonize except ImportError: print('Cython not found, falling back to the slow stringprep module.') else: ext_modules = cythonize('slixmpp/stringprep.pyx') else: print('libidn-dev not found, falling back to the slow stringprep module.') setup( name="slixmpp", version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Florent Le Coz', author_email='louiz@louiz.org', url='https://dev.louiz.org/projects/slixmpp', license='MIT', platforms=['any'], packages=packages, ext_modules=ext_modules, install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules'], classifiers=CLASSIFIERS, cmdclass={'test': TestCommand} ) slixmpp-1.2.2/docs/0000755000175000001440000000000013014656513015130 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/xmpp_tdg.rst0000644000175000001440000002474512473141653017522 0ustar mathieuiusers00000000000000Following *XMPP: The Definitive Guide* ====================================== Slixmpp was featured in the first edition of the O'Reilly book `XMPP: The Definitive Guide `_ by Peter Saint-Andre, Kevin Smith, and Remko Tronçon. The original source code for the book's examples can be found at http://github.com/remko/xmpp-tdg. An updated version of the source code, maintained to stay current with the latest Slixmpp release, is available at http://github.com/legastero/xmpp-tdg. However, since publication, Slixmpp has advanced from version 0.2.1 to version 1.0 and there have been several major API changes. The most notable is the introduction of :term:`stanza objects ` which have simplified and standardized interactions with the XMPP XML stream. What follows is a walk-through of *The Definitive Guide* highlighting the changes needed to make the code examples work with version 1.0 of Slixmpp. These changes have been kept to a minimum to preserve the correlation with the book's explanations, so be aware that some code may not use current best practices. Example 2-2. (Page 26) ---------------------- **Implementation of a basic bot that echoes all incoming messages back to its sender.** The echo bot example requires a change to the ``handleIncomingMessage`` method to reflect the use of the ``Message`` :term:`stanza object`. The ``"jid"`` field of the message object should now be ``"from"`` to match the ``from`` attribute of the actual XML message stanza. Likewise, ``"message"`` changes to ``"body"`` to match the ``body`` element of the message stanza. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingMessage(self, message): self.xmpp.send_message(message["from"], message["body"]) `View full source `_ | `View original code `_ Example 14-1. (Page 215) ------------------------ **CheshiR IM bot implementation.** The main event handling method in the Bot class is meant to process both message events and presence update events. With the new changes in Slixmpp 1.0, extracting a CheshiR status "message" from both types of stanzas requires accessing different attributes. In the case of a message stanza, the ``"body"`` attribute would contain the CheshiR message. For a presence event, the information is stored in the ``"status"`` attribute. To handle both cases, we can test the type of the given event object and look up the proper attribute based on the type. Like in the EchoBot example, the expression ``event["jid"]`` needs to change to ``event["from"]`` in order to get a JID object for the stanza's sender. Because other functions in CheshiR assume that the JID is a string, the ``jid`` attribute is used to access the string version of the JID. A check is also added in case ``user`` is ``None``, but the check could (and probably should) be placed in ``addMessageFromUser``. Another change is needed in ``handleMessageAddedToBackend`` where an HTML-IM response is created. The HTML content should be enclosed in a single element, such as a ``

`` tag. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingXMPPEvent(self, event): msgLocations = {slixmpp.stanza.presence.Presence: "status", slixmpp.stanza.message.Message: "body"} message = event[msgLocations[type(event)]] user = self.backend.getUserFromJID(event["from"].jid) if user is not None: self.backend.addMessageFromUser(message, user) def handleMessageAddedToBackend(self, message) : body = message.user + ": " + message.text htmlBody = "

%(user)s: %(message)s

" % { "uri": self.url + "/" + message.user, "user" : message.user, "message" : message.text } for subscriberJID in self.backend.getSubscriberJIDs(message.user) : self.xmpp.send_message(subscriberJID, body, mhtml=htmlBody) `View full source `_ | `View original code `_ Example 14-3. (Page 217) ------------------------ **Configurable CheshiR IM bot implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. The main difference for the configurable IM bot is the handling for the data form in ``handleConfigurationCommand``. The test for equality with the string ``"1"`` is no longer required; Slixmpp converts boolean data form fields to the values ``True`` and ``False`` automatically. For the method ``handleIncomingXMPPPresence``, the attribute ``"jid"`` is again converted to ``"from"`` to get a JID object for the presence stanza's sender, and the ``jid`` attribute is used to access the string version of that JID object. A check is also added in case ``user`` is ``None``, but the check could (and probably should) be placed in ``getShouldMonitorPresenceFromUser``. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleConfigurationCommand(self, form, sessionId): values = form.getValues() monitorPresence =values["monitorPresence"] jid = self.xmpp.plugin["xep_0050"].sessions[sessionId]["jid"] user = self.backend.getUserFromJID(jid) self.backend.setShouldMonitorPresenceFromUser(user, monitorPresence) def handleIncomingXMPPPresence(self, event): user = self.backend.getUserFromJID(event["from"].jid) if user is not None: if self.backend.getShouldMonitorPresenceFromUser(user): self.handleIncomingXMPPEvent(event) `View full source `_ | `View original code `_ Example 14-4. (Page 220) ------------------------ **CheshiR IM server component implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. Like several previous examples, a needed change is to replace ``subscription["from"]`` with ``subscription["from"].jid`` because the ``BaseXMPP`` method ``make_presence`` requires the JID to be a string. A correction needs to be made in ``handleXMPPPresenceProbe`` because a line was left out of the original implementation; the variable ``user`` is undefined. The JID of the user can be extracted from the presence stanza's ``from`` attribute. Since this implementation of CheshiR uses an XMPP component, it must include a ``from`` attribute in all messages that it sends. Adding the ``from`` attribute is done by including ``mfrom=self.xmpp.jid`` in calls to ``self.xmpp.send_message``. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleXMPPPresenceProbe(self, event) : self.xmpp.send_presence(pto = event["from"]) def handleXMPPPresenceSubscription(self, subscription) : if subscription["type"] == "subscribe" : userJID = subscription["from"].jid self.xmpp.send_presence_subscription(pto=userJID, ptype="subscribed") self.xmpp.send_presence(pto = userJID) self.xmpp.send_presence_subscription(pto=userJID, ptype="subscribe") def handleMessageAddedToBackend(self, message) : body = message.user + ": " + message.text for subscriberJID in self.backend.getSubscriberJIDs(message.user) : self.xmpp.send_message(subscriberJID, body, mfrom=self.xmpp.jid) `View full source `_ | `View original code `_ Example 14-6. (Page 223) ------------------------ **CheshiR IM server component with in-band registration support.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. After applying the changes from Example 14-4 above, the registrable component implementation should work correctly. .. tip:: To see how to implement in-band registration as a Slixmpp plugin, see the tutorial :ref:`tutorial-create-plugin`. `View full source `_ | `View original code `_ Example 14-7. (Page 225) ------------------------ **Extended CheshiR IM server component implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. While the final code example can look daunting with all of the changes made, it requires very few modifications to work with the latest version of Slixmpp. Most differences are the result of CheshiR's backend functions expecting JIDs to be strings so that they can be stripped to bare JIDs. To resolve these, use the ``jid`` attribute of the JID objects. Also, references to ``"message"`` and ``"jid"`` attributes need to be changed to either ``"body"`` or ``"status"``, and either ``"from"`` or ``"to"`` depending on if the object is a message or presence stanza and which of the JIDs from the stanza is needed. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingXMPPMessage(self, event) : message = self.addRecipientToMessage(event["body"], event["to"].jid) user = self.backend.getUserFromJID(event["from"].jid) self.backend.addMessageFromUser(message, user) def handleIncomingXMPPPresence(self, event) : if event["to"].jid == self.componentDomain : user = self.backend.getUserFromJID(event["from"].jid) self.backend.addMessageFromUser(event["status"], user) ... def handleXMPPPresenceSubscription(self, subscription) : if subscription["type"] == "subscribe" : userJID = subscription["from"].jid user = self.backend.getUserFromJID(userJID) contactJID = subscription["to"] self.xmpp.send_presence_subscription( pfrom=contactJID, pto=userJID, ptype="subscribed", pnick=user) self.sendPresenceOfContactToUser(contactJID=contactJID, userJID=userJID) if contactJID == self.componentDomain : self.sendAllContactSubscriptionRequestsToUser(userJID) `View full source `_ | `View original code `_ slixmpp-1.2.2/docs/sasl.rst0000644000175000001440000000007412473141653016627 0ustar mathieuiusers00000000000000How SASL Authentication Works ============================= slixmpp-1.2.2/docs/create_plugin.rst0000644000175000001440000006150213004224717020503 0ustar mathieuiusers00000000000000.. _create-plugin: Creating a Slixmpp Plugin =========================== One of the goals of Slixmpp is to provide support for every draft or final XMPP extension (`XEP `_). To do this, Slixmpp has a plugin mechanism for adding the functionalities required by each XEP. But even though plugins were made to quickly implement and prototype the official XMPP extensions, there is no reason you can't create your own plugin to implement your own custom XMPP-based protocol. This guide will help walk you through the steps to implement a rudimentary version of `XEP-0077 In-band Registration `_. In-band registration was implemented in example 14-6 (page 223) of `XMPP: The Definitive Guide `_ because there was no Slixmpp plugin for XEP-0077 at the time of writing. We will partially fix that issue here by turning the example implementation from *XMPP: The Definitive Guide* into a plugin. Again, note that this will not a complete implementation, and a different, more robust, official plugin for XEP-0077 may be added to Slixmpp in the future. .. note:: The example plugin created in this guide is for the server side of the registration process only. It will **NOT** be able to register new accounts on an XMPP server. First Steps ----------- Every plugin inherits from the class :mod:`BasePlugin `_. To do that, we tell the ``xep_0030`` plugin to add the ``"jabber:iq:register"`` feature. We put this call in a method named ``post_init`` which will be called once the plugin has been loaded; by doing so we advertise that we can do registrations only after we finish activating the plugin. The ``post_init`` method needs to call ``BasePlugin.post_init(self)`` which will mark that ``post_init`` has been called for the plugin. Once the Slixmpp object begins processing, ``post_init`` will be called on any plugins that have not already run ``post_init``. This allows you to register plugins and their dependencies without needing to worry about the order in which you do so. **Note:** by adding this call we have introduced a dependency on the XEP-0030 plugin. Be sure to register ``'xep_0030'`` as well as ``'xep_0077'``. Slixmpp does not automatically load plugin dependencies for you. .. code-block:: python def post_init(self): BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature("jabber:iq:register") Creating Custom Stanza Objects ------------------------------ Now, the IQ stanzas needed to implement our version of XEP-0077 are not very complex, and we could just interact with the XML objects directly just like in the *XMPP: The Definitive Guide* example. However, creating custom stanza objects is good practice. We will create a new ``Registration`` stanza. Following the *XMPP: The Definitive Guide* example, we will add support for a username and password field. We also need two flags: ``registered`` and ``remove``. The ``registered`` flag is sent when an already registered user attempts to register, along with their registration data. The ``remove`` flag is a request to unregister a user's account. Adding additional `fields specified in XEP-0077 `_ will not be difficult and is left as an exercise for the reader. Our ``Registration`` class needs to start with a few descriptions of its behaviour: * ``namespace`` The namespace our stanza object lives in. In this case, ``"jabber:iq:register"``. * ``name`` The name of the root XML element. In this case, the ``query`` element. * ``plugin_attrib`` The name to access this type of stanza. In particular, given a registration stanza, the ``Registration`` object can be found using: ``iq_object['register']``. * ``interfaces`` A list of dictionary-like keys that can be used with the stanza object. When using ``"key"``, if there exists a method of the form ``getKey``, ``setKey``, or``delKey`` (depending on context) then the result of calling that method will be returned. Otherwise, the value of the attribute ``key`` of the main stanza element is returned if one exists. **Note:** The accessor methods currently use title case, and not camel case. Thus if you need to access an item named ``"methodName"`` you will need to use ``getMethodname``. This naming convention might change to full camel case in a future version of Slixmpp. * ``sub_interfaces`` A subset of ``interfaces``, but these keys map to the text of any subelements that are direct children of the main stanza element. Thus, referencing ``iq_object['register']['username']`` will either execute ``getUsername`` or return the value in the ``username`` element of the query. If you need to access an element, say ``elem``, that is not a direct child of the main stanza element, you will need to add ``getElem``, ``setElem``, and ``delElem``. See the note above about naming conventions. .. code-block:: python from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin from slixmpp import Iq class Registration(ElementBase): namespace = 'jabber:iq:register' name = 'query' plugin_attrib = 'register' interfaces = {'username', 'password', 'registered', 'remove'} sub_interfaces = interfaces def getRegistered(self): present = self.xml.find('{%s}registered' % self.namespace) return present is not None def getRemove(self): present = self.xml.find('{%s}remove' % self.namespace) return present is not None def setRegistered(self, registered): if registered: self.addField('registered') else: del self['registered'] def setRemove(self, remove): if remove: self.addField('remove') else: del self['remove'] def addField(self, name): itemXML = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(itemXML) Setting a ``sub_interface`` attribute to ``""`` will remove that subelement. Since we want to include empty registration fields in our form, we need the ``addField`` method to add the empty elements. Since the ``registered`` and ``remove`` elements are just flags, we need to add custom logic to enforce the binary behavior. Extracting Stanzas from the XML Stream -------------------------------------- Now that we have a custom stanza object, we need to be able to detect when we receive one. To do this, we register a stream handler that will pattern match stanzas off of the XML stream against our stanza object's element name and namespace. To do so, we need to create a ``Callback`` object which contains an XML fragment that can identify our stanza type. We can add this handler registration to our ``plugin_init`` method. Also, we need to associate our ``Registration`` class with IQ stanzas; that requires the use of the ``register_stanza_plugin`` function (in ``slixmpp.xmlstream.stanzabase``) which takes the class of a parent stanza type followed by the substanza type. In our case, the parent stanza is an IQ stanza, and the substanza is our registration query. The ``__handleRegistration`` method referenced in the callback will be our handler function to process registration requests. .. code-block:: python def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) register_stanza_plugin(Iq, Registration) Handling Incoming Stanzas and Triggering Events ----------------------------------------------- There are six situations that we need to handle to finish our implementation of XEP-0077. **Registration Form Request from a New User:** .. code-block:: xml **Registration Form Request from an Existing User:** .. code-block:: xml Foo hunter2 **Unregister Account:** .. code-block:: xml **Incomplete Registration:** .. code-block:: xml Foo **Conflicting Registrations:** .. code-block:: xml Foo hunter2 **Successful Registration:** .. code-block:: xml Cases 1 and 2: Registration Requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Responding to registration requests depends on if the requesting user already has an account. If there is an account, the response should include the ``registered`` flag and the user's current registration information. Otherwise, we just send the fields for our registration form. We will handle both cases by creating a ``sendRegistrationForm`` method that will create either an empty of full form depending on if we provide it with user data. Since we need to know which form fields to include (especially if we add support for the other fields specified in XEP-0077), we will also create a method ``setForm`` which will take the names of the fields we wish to include. .. code-block:: python def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.form_fields = ('username', 'password') ... remainder of plugin_init ... def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) def setForm(self, *fields): self.form_fields = fields def sendRegistrationForm(self, iq, userData=None): reg = iq['register'] if userData is None: userData = {} else: reg['registered'] = True for field in self.form_fields: data = userData.get(field, '') if data: # Add field with existing data reg[field] = data else: # Add a blank field reg.addField(field) iq.reply().set_payload(reg.xml) iq.send() Note how we are able to access our ``Registration`` stanza object with ``iq['register']``. A User Backend ++++++++++++++ You might have noticed the reference to ``self.backend``, which is an object that abstracts away storing and retrieving user information. Since it is not much more than a dictionary, we will leave the implementation details to the final, full source code example. Case 3: Unregister an Account ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The next simplest case to consider is responding to a request to remove an account. If we receive a ``remove`` flag, we instruct the backend to remove the user's account. Since your application may need to know about when users are registered or unregistered, we trigger an event using ``self.xmpp.event('unregister_user', iq)``. See the component examples below for how to respond to that event. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': # Remove an account if iq['register']['remove']: self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return Case 4: Incomplete Registration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For the next case we need to check the user's registration to ensure it has all of the fields we wanted. The simple option that we will use is to loop over the field names and check each one; however, this means that all fields we send to the user are required. Adding optional fields is left to the reader. Since we have received an incomplete form, we need to send an error message back to the user. We have to send a few different types of errors, so we will also create a ``_sendError`` method that will add the appropriate ``error`` element to the IQ reply. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable' "Please fill in all fields.") return ... def _sendError(self, iq, code, error_type, name, text=''): iq.reply().set_payload(iq['register'].xml) iq.error() iq['error']['code'] = code iq['error']['type'] = error_type iq['error']['condition'] = name iq['error']['text'] = text iq.send() Cases 5 and 6: Conflicting and Successful Registration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We are down to the final decision on if we have a successful registration. We send the user's data to the backend with the ``self.backend.register`` method. If it returns ``True``, then registration has been successful. Otherwise, there has been a conflict with usernames and registration has failed. Like with unregistering an account, we trigger an event indicating that a user has been registered by using ``self.xmpp.event('registered_user', iq)``. See the component examples below for how to respond to this event. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable', "Please fill in all fields.") return if self.backend.register(iq['from'].bare, iq['register']): # Successful registration self.xmpp.event('registered_user', iq) iq.reply().set_payload(iq['register'].xml) iq.send() else: # Conflicting registration self._sendError(iq, '409', 'cancel', 'conflict', "That username is already taken.") Example Component Using the XEP-0077 Plugin ------------------------------------------- Alright, the moment we've been working towards - actually using our plugin to simplify our other applications. Here is a basic component that simply manages user registrations and sends the user a welcoming message when they register, and a farewell message when they delete their account. Note that we have to register the ``'xep_0030'`` plugin first, and that we specified the form fields we wish to use with ``self.xmpp.plugin['xep_0077'].setForm('username', 'password')``. .. code-block:: python import slixmpp.componentxmpp class Example(slixmpp.componentxmpp.ComponentXMPP): def __init__(self, jid, password): slixmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 8888) self.register_plugin('xep_0030') self.register_plugin('xep_0077') self.plugin['xep_0077'].setForm('username', 'password') self.add_event_handler("registered_user", self.reg) self.add_event_handler("unregistered_user", self.unreg) def reg(self, iq): msg = "Welcome! %s" % iq['register']['username'] self.send_message(iq['from'], msg, mfrom=self.fulljid) def unreg(self, iq): msg = "Bye! %s" % iq['register']['username'] self.send_message(iq['from'], msg, mfrom=self.fulljid) **Congratulations!** We now have a basic, functioning implementation of XEP-0077. Complete Source Code for XEP-0077 Plugin ---------------------------------------- Here is a copy of a more complete implementation of the plugin we created, but with some additional registration fields implemented. .. code-block:: python """ Creating a Slixmpp Plugin This is a minimal implementation of XEP-0077 to serve as a tutorial for creating Slixmpp plugins. """ from slixmpp.plugins.base import BasePlugin from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.xpath import MatchXPath from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin from slixmpp import Iq import copy class Registration(ElementBase): namespace = 'jabber:iq:register' name = 'query' plugin_attrib = 'register' interfaces = {'username', 'password', 'email', 'nick', 'name', 'first', 'last', 'address', 'city', 'state', 'zip', 'phone', 'url', 'date', 'misc', 'text', 'key', 'registered', 'remove', 'instructions'} sub_interfaces = interfaces def getRegistered(self): present = self.xml.find('{%s}registered' % self.namespace) return present is not None def getRemove(self): present = self.xml.find('{%s}remove' % self.namespace) return present is not None def setRegistered(self, registered): if registered: self.addField('registered') else: del self['registered'] def setRemove(self, remove): if remove: self.addField('remove') else: del self['remove'] def addField(self, name): itemXML = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(itemXML) class UserStore(object): def __init__(self): self.users = {} def __getitem__(self, jid): return self.users.get(jid, None) def register(self, jid, registration): username = registration['username'] def filter_usernames(user): return user != jid and self.users[user]['username'] == username conflicts = filter(filter_usernames, self.users.keys()) if conflicts: return False self.users[jid] = registration return True def unregister(self, jid): del self.users[jid] class xep_0077(BasePlugin): """ XEP-0077 In-Band Registration """ def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.form_fields = ('username', 'password') self.form_instructions = "" self.backend = UserStore() self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) register_stanza_plugin(Iq, Registration) def post_init(self): BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature("jabber:iq:register") def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable', "Please fill in all fields.") return if self.backend.register(iq['from'].bare, iq['register']): # Successful registration self.xmpp.event('registered_user', iq) reply = iq.reply() reply.set_payload(iq['register'].xml) reply.send() else: # Conflicting registration self._sendError(iq, '409', 'cancel', 'conflict', "That username is already taken.") def setForm(self, *fields): self.form_fields = fields def setInstructions(self, instructions): self.form_instructions = instructions def sendRegistrationForm(self, iq, userData=None): reg = iq['register'] if userData is None: userData = {} else: reg['registered'] = True if self.form_instructions: reg['instructions'] = self.form_instructions for field in self.form_fields: data = userData.get(field, '') if data: # Add field with existing data reg[field] = data else: # Add a blank field reg.addField(field) reply = iq.reply() reply.set_payload(reg.xml) reply.send() def _sendError(self, iq, code, error_type, name, text=''): reply = iq.reply() reply.set_payload(iq['register'].xml) reply.error() reply['error']['code'] = code reply['error']['type'] = error_type reply['error']['condition'] = name reply['error']['text'] = text reply.send() slixmpp-1.2.2/docs/handlersmatchers.rst0000644000175000001440000000014412473141653021212 0ustar mathieuiusers00000000000000.. _using-handlers-matchers: Using Stream Handlers and Matchers ================================== slixmpp-1.2.2/docs/_static/0000755000175000001440000000000013014656513016556 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/_static/nature.css0000644000175000001440000001005512473141653020571 0ustar mathieuiusers00000000000000/* * nature.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- nature theme. * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: Arial, sans-serif; font-size: 100%; background-color: #111; color: #555; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } hr { border: 1px solid #B1B4B6; } div.document { background-color: #eee; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; font-size: 0.9em; } div.footer { color: #555; width: 100%; padding: 13px 0; text-align: center; font-size: 75%; } div.footer a { color: #444; text-decoration: underline; } div.related { background-color: #6BA81E; line-height: 32px; color: #fff; text-shadow: 0px 1px 0 #444; font-size: 0.9em; } div.related a { color: #E2F3CC; } div.sphinxsidebar { font-size: 0.75em; line-height: 1.5em; } div.sphinxsidebarwrapper{ padding: 20px 0; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Arial, sans-serif; color: #222; font-size: 1.2em; font-weight: normal; margin: 0; padding: 5px 10px; background-color: #ddd; text-shadow: 1px 1px 0 white } div.sphinxsidebar h4{ font-size: 1.1em; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p { color: #888; padding: 5px 20px; } div.sphinxsidebar p.topless { } div.sphinxsidebar ul { margin: 10px 20px; padding: 0; color: #000; } div.sphinxsidebar a { color: #444; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: sans-serif; font-size: 1em; } div.sphinxsidebar input[type=text]{ margin-left: 20px; } /* -- body styles ----------------------------------------------------------- */ a { color: #005B81; text-decoration: none; } a:hover { color: #E32E00; text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: Arial, sans-serif; background-color: #BED4EB; font-weight: normal; color: #212224; margin: 30px 0px 10px 0px; padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white } div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; background-color: #D8DEE3; } a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { line-height: 1.5em; } div.admonition p.admonition-title + p { display: inline; } div.highlight{ background-color: white; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre { padding: 10px; background-color: White; color: #222; line-height: 1.2em; border: 1px solid #C6C9CB; font-size: 1.1em; margin: 1.5em 0 1.5em 0; -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ font-size: 1.1em; font-family: monospace; } .viewcode-back { font-family: Arial, sans-serif; } div.viewcode-block:target { background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; }slixmpp-1.2.2/docs/_static/images/0000755000175000001440000000000013014656513020023 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/_static/images/arch_layers.png0000644000175000001440000006577512473141653023053 0ustar mathieuiusers00000000000000PNG  IHDR Jv pHYs   IDATx |ǟA: qq-uTQTUuEJݥn& "3ɻ6Molb|>}gwn3K                           C@-l"¥&izMq|nyN-[OO%ö )" 7^C]E Ek]mS+C7;L#`UPi:fbTi]\XT]>ʜR{;t]r]>CC XOaNjmAA@싀UBFu-5%5׊Ei=싈VA'/?vu֠=J   ` - 5M_MѮʝ:N5 ֨ ʈi9t^7?w.JMVs9Eo\FkA@ 6<-L?7LjQ%xA&Jw*M}%tb=4ٶB~x_y=n~ O3W)*t!C@FOBO,ZD 6X4݂~Z6Et;۠XرyTj  `%bJ%j1;8jsuB(ܺW-}K4WbQ3H,Dz1oѵ`?RBq]#"OQS[!`Xx:}Qa&~}()(},\ٜrBQXɠ'} I {Ύ^0&-9-2@/vRfgQςFC=өѫ`3NMEd pSȝ K2wm  0z*`#ggAl 7<= s}{%IP:(xJ{Wi)Ʀq[8ObAR LaNCbi.5LʔL]zMտ'ݼv-?J-(8Z-GN;/AV9 :92  "@1(ςXD$f+=?}^/P| E8Jaa Td.?#}Ѹ2{2W/QMrT˓& C_t4wXjSF]Ge:i v@^ ^  3IGO9<ONuYFYޫ,kÕxjYn2ʅ?oDϢ^'0kZ6ooUCv@gޮ *̓J(sx9sǛ[y6*Oq.?Cc|'*׬yi*+̖B,'~meY?:v!uۻs~ê%rΊ#WiHK:1d,BUA@-Vу{(Utx}jsiϢ\H>o\i1-W9޳s>'Ԡf:ź8f*Λ_ѥ'-3?5اJ.:Ys jД|C~ F;%;p6G ?TSД#y;qfUfn̈́0->dN?mC7lsXtlrOv[L%PBh*WZ .fH$8D" q}n)8>fWQ 7 6{%{r7A,CFTFPҙGhh﮴p92",NbQa>F :l=D(YH ӠY+a}nM-#SI|L=h4}E'C~쀀@ς]1, 0Dp$ϘFLC<qpnL6=yT`,M΋J)f- ¹ǣ%;},MA|֞VnO_c/>@^ >u9e.e9#x"&IRty6^J__vpPz)A|M/PPѧ,RY@ I@,$ fߨܑ/;|ٓ5U LʣYC;3|ЕxEnTv1yٚL!ŒG>3~)R0V:-%f;gK$88eA@ aZ$(<`:7=rS[(0R|e[Y)Sl29Lوgkoy!T A@բو_‚g.!^!{c8NvGos#WbV6ĂZbK?mK|@JCk-@nٸ;:cKίhkz˺!L i!an0b!^D0pFO< жЭ(, e͖C9ԧI9B;# V8vlQ6Z}0u h]\]oǺ%4p| -"‡  @Uh={M m8=} 'b-U'1Ă %-X1   `JVЋi@S㸎CcHWl\H(B,|@85K =p4jcA@@!@,8eD#@@@ `"آd$!f K[ᵘ>!U(|%P;}ky!l ~  < ~NoĴ@/B^Sȫr10Gn|C:܍{22qO>OM)Gb j ͘osEތ?{AaoފPq~i#onܲb 1[A4%2 ] %?.9  6D@?yB<Ϳ%+O$RD|P>OTn|󌼁9Pp= 7r'5)\/~qV !?1Q[ǴrNd@ >N  XuOe:+O o, !j wsӱ?%}NOb_p-i;绛vgvg=g&Wm?͝_n~s~H+i/^9.oJ7:/#n.^G"^G&׈x}D8|H"+DQ OOA_t 鋗l_7'PN-HJL⒋T)RPc=/"=ܽ: =x"#丿2ŏWp tGvOz mcK"j GbYט=ŏqw`~VKR !DhqDE, 'cqs$sJȒA+ yS=[YWYAOKeFcbn_ ֒_>zNoEc €qͱ @  袛"2Mn& sk\\5zMN6 f]nIO|8Nfnv2ND='͆7$O&z<<"{<8{JrOҐO'4S {7B&z#Z8c?,mQyr77rev_y#mc$VR(HX1O7yrB;Vbb@X`m/N J )+v)ƍ]u.ZWBrCѸ Q" iH/G}dD&qqx?RR~E@>>wVCO7{޾{p5| | 'Ƨ[wCtk3Б.V{o,Zbxƚ"AOA,X,ci5q&e%.!QbO@L  ! nߨ :b| @@Zf-(@@Ă^X4 @@EbZ$Q8(,8MfԬ.B_4z/?_DY yN z hg\9ֺh  Ob:E =Z]{tZ#&!@BCi({ꙡ&&o `5 VC4]H1Q>{>Ȕ9URG)mԲώIEaaat#q͟ΜB.+iiڐ֥ u&[;HbZչx)Ky6>2ed$N U\wjFŲQ7mڈ&B4tu܈PCjh9gƦEDI3#qhʌ]}쥩գWZ| UY;x߄`i/m@=A @,@u bx8}sCrUˈ6NEQ=wQV'C16^mTlĂ_ =VQ5[4}@飐@4nV&xȰ<vyĂs_xDp`߱U16Lz o?+YC1{b&puQ5i 6}yr=B@z'><7XaA"ESXp#y^GSٲs=G3W_J+ * `s l| ʐU[EǍ}J MFdѻ/N"A @,8%Ė>o"hԴhޅʏ *@,娦rBKkz;j3.'&$VCqѺPtT޻2}*!Y!#v@z:WXt-+w`7=yLoB4aۨ=B⳺0r$ʞ='IE=؉CPȕ3 <ސC:p/y o3{Tp&<=Dz4k+DXq^/v wP:td/e˚]>=p.K_ϟw?t ":a|mrٹk!YM..4uϔ:{9s qQ16}ڵ*O/B|!4LZ{8   3d. 3m\ԫG?lweݸy/{K^ֹJBg9{RTT3Z|\ZK#D9r@\> `7 R~c[ʚ#dyܡ]W ԡt7g3RS Ta99{"E+C˾nE^':91=9Eh},ի֊3Z8 C"IT)-E?}S[Ky!;r̤ͪOxXȓ;b'E(R=yD Ρ+拧x~L'Ǹ ,!<7zeʘYJcFN}M,&!Ga/eJ<&4}jzKҐ(S|{) 1o}r.ry/!mq/r^ŨTzMid@._{w_|yh>#Yr= uUϛ=m?j׬Q{Jp0⟠,뜐@s/pzVB( =)íHi$ ֥햠W PC!OWIV/&^*p0RjYU>]`|@*Xxqt]_0j@vOo<j3aeREŋxEEUvط@峅,4ERN۲ƻF%(wJo%>wҕhǚK84Cz["\Ym։`_.^!SĔ8=%i=34]":MџJy{렩A tl3*.y7G<} @> xR )C挦&8#u(-8N`Ԅc5 ZpX=aa {a)RQ}w)xM6nZ  j@! {qZtiK2D E/bnE{|umxWAb!8HpMxgVXڑ p9GJg"%g6*XӸMVGM!Ђ-cp #c^a*@‘ӷuZM5P Ђ-C)șa]D4 $7{AcB@ςU0p\ {m2 `D!    ڢe   ` VB@@@q ER{m2"}.uz:s|I&u-ZQZ46TSʅdb|3mؾ x=kOiH{)ݺs>o)4f2SY^.yT`w\vص\Wn\#] eN1/pѺGtTٻ =_АG!cg!t EA=iI ~iqTq-^炒TfJ4t(FL-mȤR(ə& |UǏCs'Z)~ =ЃG̙ĈEP'Ըg1"eM]DCA@Ǡh=k6ze wغz}Ս=N{ع k뗭_mew՛֓ ͛#N-o9i7m6<?B9}dmvṛ|^[Py<Xs<-o-Q@"G@,$2` v'{2eH;Vng7)\iP߄&4yA%b+)Gb!gz%T,ܸ,*KLJqnY{&YD'<cK~B/_GЖ GĞn\8GmN\~ݿ}G#nttns"uOoLC%],ϳuyK}BómK.=d9<D s/BlҴwqTt!)X[W+AW-+UFV(M~M{xPE!3{(08>GzX¨Ϩ!ҾVqSkgaVJXH ≖N'- MuyhY=(iҐQTPJ"ukI´(R4|+B8< ~*~گ]y*xyӔacdrԇg)ۭ]'Y^w]TɓӢɠp9sW3/"8BX JƟЎM^V0g!x%PtYy#n8 9y 5F"l^Z>+:ly4h5Zb_qP,b\P̥q/!XjG7t Vl ~\ђ%Z褺J?o&R挑xQi#I;EL2(C3Γ>*W0*w1 a e &ϒkb@_M59 ܍ 'F0Bݍ{?sL԰}h-QKҝvM'P6/Jʡ[9ޛ%J=0~ j`"i%WrfHڻfKsŗZ,d8])Fv;x֮!.偀]N}߄fjÂ瀻ydSp_Ѷke59UE\] 1•ϓs[Mncz=X\LOT N<ݔ {&{bر]7lq++3'/jeR<{o_Rej{e!of?E5?2DzmKP-{fhDu73kL1"6OQ-:6! +ÜyPi:9glKwk_v,8Ŵ 䨧ALSp C )DB4 D$phUkʨ)$p7!&[Oe0g%W>Z҅bqDw$Î.BRPsm'MB0BLz .ā2*8PДD$p\ߝeg յ?7Еe0y pf |ڮiv{w#%ce9KucPy >t[<&Q:ڜbQ'H'/A1B.5LPIKby}xKrIוb# 2]N<_K @,XJ "pIJ?'h5x]Q(k+ڪqq"QLA Nk_A,O / nQia@{#3{F(.n"( 8(!ݏ1 `ކKGԴbA -'d4a5iBZh/WqMK _:j{6g/8gT-Nfb!@(1SJMTr䝿2}Z3=gzppO į+Rߵ ? GOPCcmG1%;?=tv5̞HZxHWN_ WDLj3$W'_Qȋwo޼K5oVP欙g޴g>BRW!wɖEC:Sh|TaUR͋gHF~]'eY 5+Q(|6<.]C&Kh4aX)}oS< dmfM_AYe$|9(]4qT20{ݾuW CzڤG7l Iz?`{SE)GY  t"yF3v ͋'#=͟$d6&siԨ'y,$(!&韹$x|yj|X?ћUѯgW߷.[jqV?!""UKn$N@n /Qq[ݟ6ؼXȔ9#ݿH<6 3 9RR&e_s.Zd ]>^ E Yk\~қRtoˁiIwLIҀ5*2hs2e(_|?cb=~ZrW9N'5bXP,-rj:sqWj q@y ZMH8E|B>ixQԾWYS&.[%OfQgNMsϏf{/>-!_cA)i_&@;yX "rʲx9DOr> GY +l~6gZt҂Sf_ZvK;;rPz}xi'9ENӒ d[?y xҮc[}pƟgQT)]B6!5A@@|Cjca#ɣԱtqZh5-۰2e1Kxq+peڲ~t!|   `kB,(Jx'~h?cb' ~ebm#޾X0 b4J*R9=tQ"2hڰzK?i5 9"`b˳~CYM.b#"3x y:עE RŨiOcc!bر?CN=l;fHj'rh@@lMߧekAb 'r&V#M8%$~۾2dzk3p*qќ Pn to-2f@sͤ2Lq$H]-I&y ST?IE T.j5tUtn{~fE+ZZavpd?\b0_;R.}dzuV+"#Cbq i?7[yvR(pO] .絤naֻzcr5j.gP >B(x-BbG  .Fj:m rwCzPhܥbcڌSs 5܄NIu֢±qC4%.*5*Fz;Tlv: >o%rD>ٴ%«!/%c]zsQzˑ"eW(@O"޺E:CO5C{ CR|Cc%lZ,px1*>iXTܙ q&#ذY}Խaq̆p<]ěuTx!m'u;m jiIP絤nٸ)Elk{R_lAԯq8 GYNJ-߰6Eo޾RWB_ާ˓Z{fJ*,H i|6DA/ѐ)EOm WӖ) N  }(cԌ! @@{'LL鯦|Zѥʨ60}up~7G h V"@@Cȗ;'͟0x+<銼J||4!uXHAOO]Orq}60"=Óo詉{QŻ rlC~-al /rVu  eD)A+z޽AC,X"4CE]֩%dh8JDȖڶC,%b KmMk۴#m'СE3@J- >ˈYSo@@wji}̿}1f۴Xػs};Z܇(CUդo:{mN,%?K+'b'{ Z׵|Re@@@%^d"2"_ZMMPs%y?슖Wgd&6 PɣAO!h:^{xoM66ʔ1c0_3D#tOiqI0 iҺS>=J(kS:o5im};HcqcľP ^@!|(cz{6   @DNXaw9HKkwJ{~)Bd5$7$>c#!]ؒ  \޽YUg͉~^)_f/ N=:ʴu+# _uDeV#/Rl,pc~:Rew9qJwi`TH Ģ%mYMI3a|NG3'͖/Dm n4"U")p椟?{KZ? G쮉R0uI릆xӝicf9K oS票cǯҰ  Dz(̕#NN=s{bLeŀ3۶#WvWJ/yy [XKO7l6b (FO0dQmy͆alX<(i&.-: ֆO/ߐŚ[Cy   lr":Zo#/^XbCp5+*R  8X`HujBX0|@@@$Tq@"{zmq!RŊQk'}G;D ) x%}޸!OM[1Ӧ˴R%z+ũAfb}/~2ں^H7]?0kŅҥ RB < Xz Ł#7'Oҷ:ҩcG%|cG9r1b)rMFK{޷I|fEˣ̛#-x>zVAOcӗ-jL>|!q…gM[( )4mQb $`M( @@WI͘)}?hxϐ!wʔ//KVw@y9.)UUV?{FFK;u^ ժE@R.OO޻K/iԔiY,?p[:@3|^@<ǥshOJqb8Mtab!3 t[JUYƑ۾ eWjyς?7n̙x o0x  ` W>жӧmJU!t?423*|%/}* -V/["+"BI2fN:}mصJovr##ʕoic?]bfGqr}91~,lF(M<$ qtY`S4>wۦSO}D<@~N,~](nCȁ#:Y2^y:svQF[(fW+/PO[ґ! /yXtCaZFB򘖁c,X"00?r M&ؿgRz 5/g۲7ꄈ0}nn>mhjՠ.-;ބnߒ쯠jWʭ'4r%O̍?|(ݿVjC< %[6*Oi;ϕ<A= ֠2@HN7PhXhᲷgԪW_ nX/F9 6*5jʞk~#}Ii/H/ 10D,3jԩ+PN8NCכNNT썷^+P);NI>(!!yԞ ` ,XB 6 EEb!#M=  M `#ϫ%WKLC)/g2O?+3PBUڕ+͹@T jydɤY vPOP=:|!y5IE N"ׁ^PiĄIFe(B8(S*͘" !R! `bsK,rOVҧ~}t8?p"E8t 2"?ƶ*V{~+)Ν>Em426!v`3dtCocvfC<\pX Uan%GY/151E@g. F5ʑgA,]QNe6 5F)LJ(% ^6QhO40wK^xebH] NLePb4N ^{5;l<~ݾMG0MB)EL /0ja6m۬\GC7=%iS{W_"VӨaTx1d8k7 7Q6 = 8)'h6XJbRR'%K @,XJ v    £   `)KI@@Ă^x4@@,%`))؁Xp f ,%;pR NzlĂ`  NJbI/<   X@@@I @,8GA@@R 8)'6\4@@Ă-I( h80g/^DNXM3!rT[>0(Q[vOEq5ڛܴh/A:U? si ۷ %{.Չ#4bA -}}|?C~:9꠭D@@ :t8%ܴ6RDD$$|m!9j)@,%/hWy~Exm% I`/0h˓/[<-E@@,^. ..xPs!ͽ(ii}R">QBu{q#"//@'/~wKM+;Lw#M AA_ji2x,I)fʙLIgW]D?x@/)( /^ǏS?z7r/)"V8e" 9< ?ރN&cFԼ9Unْ׏l|8<G+^LG,J 9 gY($K>ٓmB #^p @(T#!Nק {hx.  营:*W}uL,@@l~6l}p WM<EQKkPt2X(FS4ͽT:{R-E@@qV ފB*=  ZKDN(RW~ ǏGo H\},E0d={B,=KBZ4-SItb!$ZNKI9])h"Z j@@Ă]p4@@XPK   d 삣   ĂZb'#d =8'h.%A@@@,8GsA@@@-`  NFb.8   j @,%{p2 Nv\PKbA-1؃Xp Z j@@Ă]p4@@XPK   d 삣   ĂZb'#d =8'h.%A@@@,8GsA@@@-`  NFb.8   j @,%{p2 Nv\PKUm؃BavN*'YR)I+ܮԮI+7h@^EP…eT@Ce_ѹW͛tmv%˕!Nw~Z:vRd(\ԨF m[J&!kd  EH0ѱs4xt9d~SJ7G6`!APy@Ə7);,Cm;@ ˗JF~B׭[ŋ)[LԦo_zY*B b@  7h E͛ŅKR(3{v##thioi3kh 98m +VcWSqӟB% "u3GB@<\  `!^^ԿsgiSCE OJ4[R-q~N]-{*Bo8~,bɸqT`AJ*J-KKEB< Mb!n>H" ?}J<ca~K IV ϒ!qCw ؈}"e3vmjҳOTY3P21?Lo0iشg5уPJ(m^LsqJɓ)s m1쓥Z5){%E@"XHD(n <ܰ ቅG,_bbѣL؟} v*nح},}| 7/o۳?}29eC-υhhDp|B,s~UPvObZ8x6;,VnB\/H  IA' 7gNXliҥ)I+dHJ WУ'O݇0sZbH#mZyc/E׬){_/o|cF7>"#!bXq%fW|ʗ(A~8)nu6if _  e7m;W)> ˰ca7XԗCo1d" .c@԰Q`V%`U( ?bjѻkvEz9!e",0h%g^ش._N~K0 'R޹WQqwo(Ҩ'{b3ZקwSnԮ *DLXde ԅ iQeV}Ζ+.cs<)bf'6gS;*O}T *}U4̺rot4Nv4a҇@ٿ_.Y"Fxŋq Cn bP~m$ok1M3wNnc''G{}Иx_ΞEPc6?v?y_C NjB @g z0Kg ljkA/h @2iv6뼁kٯw{dQmjHzP#}ؖ--o -b"X*=ӺR[" IDAT Vq`m[qkn,_M/ ,|Opk\_>z쭷2k͢O~Kz8Dom\orYrɧ@D|Rlb!kA DT(SmA%c7DSE._D#aQi"^묆L3̦Lm_E] &2J]`S xYeo6@`B@P x wrNz}5[u@c+9)k!i5_ !E uꦙ53O_]R|Dw{hfq7E,jДII-d$x>1sC7/i:eefbõ eEBg>al6-`/zR5fK; 1J$mkeh2Yl{ Kup%W{lhla<:֭pikL(iyfAv+nj:5 R]M5S^k6%Eu/yȒY",L/F:LTrqQg @=>jce#J ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ*~55(%E X b%փh Z֒Y聕J D8ZZ_\Nb!,!/Ѳ#.iDD,%u]@@y}9$RuNb69a%m|u9 IIcYRP`7vc C|[~a,Er Hx%7{cI8!z E" cNJrMC@ p?\JDb9^!YmNUi'}XRExqѣRp 9d_\NbanB!(5m  Z+rPISYع܌\8@ݘwr(Nbdm7gm M̖6$ʶ_\vzX C`]4c-XHטlVwn]y63M(lz\9@݌(:{%g+8/.‰>IH`=>;Hj:1hJahq@ UV|i%5v ^]:š ݹ׼ZRsޙ=qB@7%`O=^?Q&+bLb`mI^X{X(o/4.纘 i6EY$ryYz@l"x n V-XO>XQ#ϱ9$aƾ}(I%-;~'5607Օ_"ZQvvA2d( 8!gA뚝 LX*ec %'ʒFVW9; ɐs aC,%G˴"ȉ =tAG6I3ϔk2C@)k.iiv"%+1Y¤}u6^d'9eyr~%pv|shf$q𭇿<ުUT0\]`V jIHhտLׂB+͓>@1}Oym^@OʍG^ͪ"2fX cbT{r=\滘^0lS``&ʠdI:C@y=YCrpk ~B@lԔs"yZSuKg/l[\L\ZvqXI+t}BbxH \ uUWĔ[ Jƍb /z"93Dm ؈&²B kz5?7܊h08dW;zk^ԭ(6q:oH7"* aMy7 '-{@D!m=Vv*|Ps"o.b2k{-ԁ ]5"ǫ/VƎLCuqi{9E^/ٟo,/pm|ךc&EBo{3$Z .g%%IBo @]TUVzBٟZ%lE݀۾nbʰPp>}݁@mm|wg*3 v¾:Vk^5 ܹ x%J Wo$v4v ~'9:z qkLϳu-`ÈSޯT+č'rw/7##1AV v΃,[.J 2f9BcpZ8VNC @ @l=$IENDB`slixmpp-1.2.2/docs/_static/images/from_&yet.png0000644000175000001440000000537412473141653022436 0ustar mathieuiusers00000000000000PNG  IHDROr1ztEXtSoftwareAdobe ImageReadyqe< IDATx TTe޹b̀D@@E(US1Z [RVY ߪD4$-U:@ A!m 1:`Re<kWf}ごxl=+**,z{:ۼxQ6@5>>To="SK$܋& Hu-Pug'tNߞ7Xk?Z9ԠX\1\Vlyfs/NYeh.Uaa. D0vg)+H'a$,NQN(prB&E<r5[9t;rZnmwI;#χszƣn/&[L˅R5U*C{Rf_=0o@_ӥ/=c8!`kn=le c䇼DMU@PG'Mݶ1QǏL֑V|CطHvdazl7@zn0-_ܐ}Իw:7(O[{B~ 6貸a3_ۦzAGhp㽯D/i ťg/NW,dA ɰ+~9-_jz乚||%BJ?x-8n}%*}9}EC}y r?ɤ±}i:EnΝa)|5%V2 P<׽sf+{J;f4iQz~ü!S(iVE[k_j6hVކ揯|[&S +0FSa]ahx8C g+}ס*lf͏)YTz7q( RIfWE~*]U@}oN^(Y 'A,KJؓ sUhuRYcY|_ҩ5 /ܻpݼ\Ƹ*$P LEuKƖn[;rK[Lp37Ϋ(.3`hhEOn}NCBq9Xzuhy"q>90*'#$iIمxj#Y(>QtjbB3U-m]]ŰZp0/j4jQa{y=9oGd:^-st6im2q6BGfamtx,&+|THl⚌P1L;=Q䇟x:?= :;t x\4rN'|F+c+w j"xqIr푻۾ U[YaQsa;;)lEiiTS ni<ϑmY!$T٨gxa7rʗpzIHNÓ~J|oɋp=漻wS('WtƐ-:6/Z9]-n(2sW1˸-sM_Y91\L/~Nծ^vz)j8rsT¯]1rڗ Fe@Gm2K2ĈPloV.?jXp~dhM9OaUwy$rTuK t. Cjh缆E*MY = >,qGN@ܣ;ozȑyؖeC63̘nCt}4uqVrvIr07pa(yCa'Pj +8Wb}~ahqz22Ѝ:)Q?kr2 ǻB<q'i+Q쿷T_ V5L8Zr,mk]Z\4 5oDcMб2 G`=]ɂi|yjZjxݑ=ꔃjJw< Ζ” 5xq`/2|Xy}m1"|?.w}uY~!Ur$ @az LY\dx"3,=ޑ) O!?x~=7ﮓ 0Xw9ҢIENDB`slixmpp-1.2.2/docs/_static/pygments.css0000644000175000001440000000776112473141653021153 0ustar mathieuiusers00000000000000.highlight .hll { background-color: #ffffcc } .highlight { background: #000000; color: #f6f3e8; } .highlight .c { color: #7C7C7C; } /* Comment */ .highlight .err { color: #f6f3e8; } /* Error */ .highlight .g { color: #f6f3e8; } /* Generic */ .highlight .k { color: #00ADEE; } /* Keyword */ .highlight .l { color: #f6f3e8; } /* Literal */ .highlight .n { color: #f6f3e8; } /* Name */ .highlight .o { color: #f6f3e8; } /* Operator */ .highlight .x { color: #f6f3e8; } /* Other */ .highlight .p { color: #f6f3e8; } /* Punctuation */ .highlight .cm { color: #7C7C7C; } /* Comment.Multiline */ .highlight .cp { color: #96CBFE; } /* Comment.Preproc */ .highlight .c1 { color: #7C7C7C; } /* Comment.Single */ .highlight .cs { color: #7C7C7C; } /* Comment.Special */ .highlight .gd { color: #f6f3e8; } /* Generic.Deleted */ .highlight .ge { color: #f6f3e8; } /* Generic.Emph */ .highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */ .highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */ .highlight .gi { color: #f6f3e8; } /* Generic.Inserted */ .highlight .go { color: #070707; } /* Generic.Output */ .highlight .gp { color: #f6f3e8; } /* Generic.Prompt */ .highlight .gs { color: #f6f3e8; } /* Generic.Strong */ .highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */ .highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */ .highlight .kc { color: #6699CC; } /* Keyword.Constant */ .highlight .kd { color: #6699CC; } /* Keyword.Declaration */ .highlight .kn { color: #6699CC; } /* Keyword.Namespace */ .highlight .kp { color: #6699CC; } /* Keyword.Pseudo */ .highlight .kr { color: #6699CC; } /* Keyword.Reserved */ .highlight .kt { color: #FFFFB6; } /* Keyword.Type */ .highlight .ld { color: #f6f3e8; } /* Literal.Date */ .highlight .m { color: #FF73FD; } /* Literal.Number */ .highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */ .highlight .na { color: #f6f3e8; } /* Name.Attribute */ .highlight .nb { color: #f6f3e8; } /* Name.Builtin */ .highlight .nc { color: #f6f3e8; } /* Name.Class */ .highlight .no { color: #99CC99; } /* Name.Constant */ .highlight .nd { color: #f6f3e8; } /* Name.Decorator */ .highlight .ni { color: #E18964; } /* Name.Entity */ .highlight .ne { color: #f6f3e8; } /* Name.Exception */ .highlight .nf { color: #F64DBA; } /* Name.Function */ .highlight .nl { color: #f6f3e8; } /* Name.Label */ .highlight .nn { color: #f6f3e8; } /* Name.Namespace */ .highlight .nx { color: #f6f3e8; } /* Name.Other */ .highlight .py { color: #f6f3e8; } /* Name.Property */ .highlight .nt { color: #00ADEE; } /* Name.Tag */ .highlight .nv { color: #C6C5FE; } /* Name.Variable */ .highlight .ow { color: #ffffff; } /* Operator.Word */ .highlight .w { color: #f6f3e8; } /* Text.Whitespace */ .highlight .mf { color: #FF73FD; } /* Literal.Number.Float */ .highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */ .highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */ .highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */ .highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */ .highlight .sc { color: #A8FF60; } /* Literal.String.Char */ .highlight .sd { color: #A8FF60; } /* Literal.String.Doc */ .highlight .s2 { color: #A8FF60; } /* Literal.String.Double */ .highlight .se { color: #A8FF60; } /* Literal.String.Escape */ .highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */ .highlight .si { color: #A8FF60; } /* Literal.String.Interpol */ .highlight .sx { color: #A8FF60; } /* Literal.String.Other */ .highlight .sr { color: #A8FF60; } /* Literal.String.Regex */ .highlight .s1 { color: #A8FF60; } /* Literal.String.Single */ .highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */ .highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */ .highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */ .highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */ .highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */ .highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */ slixmpp-1.2.2/docs/_static/basic.css0000644000175000001440000002024712473141653020360 0ustar mathieuiusers00000000000000/* * basic.css * ~~~~~~~~~ * * Sphinx stylesheet -- basic theme. * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ /* -- main layout ----------------------------------------------------------- */ div.clearer { clear: both; } /* -- relbar ---------------------------------------------------------------- */ div.related { width: 100%; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } /* -- sidebar --------------------------------------------------------------- */ div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; } div.sphinxsidebar ul { list-style: none; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #98dbcc; font-family: sans-serif; font-size: 1em; } img { border: 0; } /* -- search page ----------------------------------------------------------- */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* -- index page ------------------------------------------------------------ */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* -- general index --------------------------------------------------------- */ table.indextable { width: 100%; } table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #f2f2f2; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } div.modindex-jumpbox { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; margin: 1em 0 1em 0; padding: 0.4em; } div.genindex-jumpbox { border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; margin: 1em 0 1em 0; padding: 0.4em; } /* -- general body styles --------------------------------------------------- */ a.headerlink { visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } p.rubric { margin-top: 30px; font-weight: bold; } img.align-left, .figure.align-left, object.align-left { clear: left; float: left; margin-right: 1em; } img.align-right, .figure.align-right, object.align-right { clear: right; float: right; margin-left: 1em; } img.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } .align-left { text-align: left; } .align-center { clear: both; text-align: center; } .align-right { text-align: right; } /* -- sidebars -------------------------------------------------------------- */ div.sidebar { margin: 0 0 0.5em 1em; border: 1px solid #ddb; padding: 7px 7px 0 7px; background-color: #efefef; width: 40%; float: right; -mox-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; } p.sidebar-title { font-weight: bold; text-transform: uppercase; } /* -- topics ---------------------------------------------------------------- */ div.topic { border: 1px solid #ccc; padding: 7px 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* -- admonitions ----------------------------------------------------------- */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; } div.body p.centered { text-align: center; margin-top: 25px; } /* -- tables ---------------------------------------------------------------- */ table.docutils { border: 0; border-collapse: collapse; } table.docutils td, table.docutils th { padding: 1px 8px 1px 5px; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #aaa; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } th { text-align: left; padding-right: 5px; } table.citation { border-left: solid 1px gray; margin-left: 1px; } table.citation td { border-bottom: none; } /* -- other body styles ----------------------------------------------------- */ ol.arabic { list-style: decimal; } ol.loweralpha { list-style: lower-alpha; } ol.upperalpha { list-style: upper-alpha; } ol.lowerroman { list-style: lower-roman; } ol.upperroman { list-style: upper-roman; } dl { margin-bottom: 15px; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } dt:target, .highlighted { } dl.glossary dt { font-weight: bold; font-size: 1.1em; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } .refcount { color: #060; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } .system-message { background-color: #fda; padding: 5px; border: 3px solid red; } .footnote:target { background-color: #ffa; } .line-block { display: block; margin-top: 1em; margin-bottom: 1em; } .line-block .line-block { margin-top: 0; margin-bottom: 0; margin-left: 1.5em; } .guilabel, .menuselection { font-family: sans-serif; } .accelerator { text-decoration: underline; } .classifier { font-style: oblique; } /* -- code displays --------------------------------------------------------- */ pre { overflow: auto; overflow-y: hidden; /* fixes display issues on Chrome browsers */ } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .viewcode-link { float: right; } .viewcode-back { float: right; font-family: sans-serif; } div.viewcode-block:target { margin: -1px -10px; padding: 0 10px; } /* -- math display ---------------------------------------------------------- */ img.math { vertical-align: middle; } div.body div.math p { text-align: center; } span.eqno { float: right; } /* -- printout stylesheet --------------------------------------------------- */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0 !important; width: 100%; } div.sphinxsidebar, div.related, div.footer, #top-link { display: none; } } slixmpp-1.2.2/docs/_static/haiku.css0000644000175000001440000002001312473141653020367 0ustar mathieuiusers00000000000000/* * haiku.css_t * ~~~~~~~~~~~ * * Sphinx stylesheet -- haiku theme. * * Adapted from http://haiku-os.org/docs/Haiku-doc.css. * Original copyright message: * * Copyright 2008-2009, Haiku. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: * Francois Revol * Stephan Assmus * Braden Ewing * Humdinger * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); @font-face { font-family: "Museo Slab"; font-weight: normal; font-style: normal; src: local("Museo Slab"), url("fonts/Museo_Slab_500.otf") format("opentype"); } @font-face { font-family: "Yanone Kaffeesatz"; font-weight: bold; font-style: normal; src: local("Yanone Kaffeesatz"), url("fonts/YanoneKaffeesatz-Bold.ttf") format("truetype"); } @font-face { font-family: "Yanone Kaffeesatz"; font-weight: lighter; font-style: normal; src: local("Yanone Kaffeesatz"), url("fonts/YanoneKaffeesatz-Regular.ttf") format("truetype"); } html { margin: 0px; padding: 0px; background: #FFF url(header.png) top left repeat-x; } body { line-height: 1.5; margin: auto; padding: 0px; font-family: "Helvetica Neueu", Helvetica, sans-serif; min-width: 30em; max-width: 70em; color: #444; text-align: center; } div.footer { padding: 8px; font-size: 11px; text-align: center; letter-spacing: 0.5px; } /* link colors and text decoration */ a:link { font-weight: bold; text-decoration: none; color: #00ADEE; } a:visited { font-weight: bold; text-decoration: none; color: #00ADEE; } a:hover, a:active { text-decoration: underline; color: #F46DBA; } /* Some headers act as anchors, don't give them a hover effect */ h1 a:hover, a:active { text-decoration: none; color: #CFCFCF; } h2 a:hover, a:active { text-decoration: none; color: #CFCFCF; } h3 a:hover, a:active { text-decoration: none; color: #CFCFCF; } h4 a:hover, a:active { text-decoration: none; color: #CFCFCF; } a.headerlink { color: #a7ce38; padding-left: 5px; } a.headerlink:hover { color: #a7ce38; } /* basic text elements */ div.content { margin: auto; margin-top: 20px; margin-bottom: 50px; font-size: 0.9em; width: 700px; text-align: left; } /* heading and navigation */ div.header { position: relative; margin: auto; margin-top: 125px; height: 85px; padding: 0 40px; font-family: "Yanone Kaffeesatz"; text-align: left; width: 750px; } div.header h1 { font-size: 2.6em; font-weight: normal; letter-spacing: 1px; color: #CFCFCF; border: 0; margin: 0; padding-top: 15px; font-family: "Yanone Kaffeesatz"; text-shadow: 1px 1px 1px rgba(175, 175, 175, .8); font-variant: small-caps; } div.header h1 a { font-weight: normal; color: #00ADEE; } div.header h2 { font-size: 1.3em; font-weight: normal; letter-spacing: 1px; text-transform: uppercase; color: #aaa; border: 0; margin-top: -3px; padding: 0; font-family: "Yanone Kaffeesatz"; } div.header img.rightlogo { float: right; } div.title { font-size: 1.3em; font-weight: bold; color: #CFCFCF; border-bottom: dotted thin #e0e0e0; margin-bottom: 25px; } div.topnav { position: relative; z-index: 0; } div.topnav p { margin: auto; margin-top: 0; margin-bottom: 0px; text-align: right; font-size: 0.8em; width: 750px; } div.bottomnav { background: #eeeeee; } div.bottomnav p { margin-right: 40px; text-align: right; font-size: 0.8em; } a.uplink { font-weight: normal; } /* contents box */ table.index { margin: 0px 0px 30px 30px; padding: 1px; border-width: 1px; border-style: dotted; border-color: #e0e0e0; } table.index tr.heading { background-color: #e0e0e0; text-align: center; font-weight: bold; font-size: 1.1em; } table.index tr.index { background-color: #eeeeee; } table.index td { padding: 5px 20px; } table.index a:link, table.index a:visited { font-weight: normal; text-decoration: none; color: #4A7389; } table.index a:hover, table.index a:active { text-decoration: underline; color: #ff4500; } /* Haiku User Guide styles and layout */ /* Rounded corner boxes */ /* Common declarations */ div.admonition { -webkit-border-radius: 10px; -khtml-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; border-style: dotted; border-width: thin; border-color: #dcdcdc; padding: 10px 15px 10px 15px; margin-bottom: 15px; margin-top: 15px; } div.note { padding: 10px 15px 10px 15px; background-color: #e4ffde; /*background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat;*/ min-height: 42px; } div.warning { padding: 10px 15px 10px 15px; background-color: #fffbc6; /*background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat;*/ min-height: 42px; } div.seealso { background: #e4ffde; } /* More layout and styles */ h1 { font-size: 1.6em; color: #aaa; border-bottom: dotted thin #e0e0e0; margin-top: 30px; font-family: "Museo Slab"; text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); } h2 { font-size: 1.5em; font-weight: normal; color: #aaa; border-bottom: dotted thin #e0e0e0; margin-top: 30px; font-family: "Museo Slab"; text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); } h3 { font-size: 1.4em; font-weight: normal; color: #aaa; margin-top: 30px; font-family: "Museo Slab"; text-shadow: 1px 1px 1px rgba(175, 175, 175, .25); } h4 { font-size: 1.3em; font-weight: normal; color: #CFCFCF; margin-top: 30px; } p { text-align: justify; } p.last { margin-bottom: 0; } ol { padding-left: 20px; } ul { padding-left: 5px; margin-top: 3px; } li { line-height: 1.3; } div.content ul > li { -moz-background-clip:border; -moz-background-inline-policy:continuous; -moz-background-origin:padding; background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em; list-style-image: none; list-style-type: none; padding: 0 0 0 1.666em; margin-bottom: 3px; } td { vertical-align: top; } tt { background-color: #e2e2e2; font-size: 1.0em; font-family: monospace; } pre { font-size: 1.1em; margin: 0 0 12px 0; padding: 0.8em; background-image: url(noise_dk.png); background-color: #222; } hr { border-top: 1px solid #ccc; border-bottom: 0; border-right: 0; border-left: 0; margin-bottom: 10px; margin-top: 20px; } /* printer only pretty stuff */ @media print { .noprint { display: none; } /* for acronyms we want their definitions inlined at print time */ acronym[title]:after { font-size: small; content: " (" attr(title) ")"; font-style: italic; } /* and not have mozilla dotted underline */ acronym { border: none; } div.topnav, div.bottomnav, div.header, table.index { display: none; } div.content { margin: 0px; padding: 0px; } html { background: #FFF; } } .viewcode-back { font-family: "DejaVu Sans", Arial, Helvetica, sans-serif; } div.viewcode-block:target { border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; margin: -1px -12px; padding: 0 12px; } #from_andyet { -webkit-box-shadow: #CCC 0px 0px 3px; background: rgba(255, 255, 255, 1); bottom: 0px; right: 17px; padding: 3px 10px; position: fixed; } #from_andyet h2 { background-image: url("images/from_&yet.png"); background-repeat: no-repeat; height: 29px; line-height: 0; text-indent: -9999em; width: 79px; margin-top: 0; margin: 0px; padding: 0px; } slixmpp-1.2.2/docs/_static/noise_dk.png0000644000175000001440000005435312473141653021073 0ustar mathieuiusers00000000000000PNG  IHDR<qiCCPICC ProfilexTkA6n"Zkx"IYhE6bk Ed3In6&*Ezd/JZE(ޫ(b-nL~7}ov r4 Ril|Bj A4%UN$As{z[V{wwҶ@G*q Y<ߡ)t9Nyx+=Y"|@5-MS%@H8qR>׋infObN~N>! ?F?aĆ=5`5_M'Tq. VJp8dasZHOLn}&wVQygE0  HPEaP@<14r?#{2u$jtbDA{6=Q<("qCA*Oy\V;噹sM^|vWGyz?W15s-_̗)UKuZ17ߟl;=..s7VgjHUO^gc)1&v!.K `m)m$``/]?[xF QT*d4o(/lșmSqens}nk~8X<R5 vz)Ӗ9R,bRPCRR%eKUbvؙn9BħJeRR~NցoEx pHYs   IDATxl [o&4LyʄΧoT.T{KKKK:&p~?G}ŝׇO>dqgy0cf~>gf@J/zS  B@dVL1N0R [L֐bkrU[S!gjxt!Ќ[hQ?b0q~mūmp7 )^==Skv8[wgz#/fo7y0ۉ dDvW;/V"`A-z,FW^g;\z1r Rmw+-r =MEu/O,=l9L>]=x5,(ǽF-jUl~}M2AD1[c181wg .X5{pa9Ӱ3M .;!F>,x‡qV񩋇5ы0v Fn}Q gsw8Nu<=þ}@pUnFs+C5P1"ݑ1A>{Ml/ ΰs 6k/'O~xU\񸛅'u۹p꟯ _|pS.9tᔫ<͍~{Z_noNxՐǼ(4qü`;㾽X&JXw SHCH 4"1{R[oEߞ<:y"IP rր x +ڋCmLan 8]?k0~1ҬVuq_X~jm]b-Ϲ^gf^nZpX89|rp": 4҆>YPYx)v~& 9C!)!D!6@B]<Q.n zt3>uY/Vp˳ދϷE'^qE-XK 7>;nO<J sáGV[\Z!Q~fR_LԘ9z(L"{/U"kPb(g G@vw1BkLn&h3Ca5HN@;<_KXZa-KƞZӄ\:wp2u7 8,g'O3=;_}qk`c=)Xar0B""T[(L>\Ay0iic ;~83 K>řs񛃘z`r+t%/}6kz,O/sWbAϷz4coB'#AXi T388:b1RA3jNZiv䉧_Xgsp3uӬFO?\_ҕ0348ӭ.f~zlj^SswSWpFD@*bgvpRݪ5%ǹzY⤅o`jhېGC'4nH8Ի85m7,? &f6p gҗ)_.0zqWO]bxiKC^N6/5N"pog1g^-pÕ۟棯.6 O}5 ^v\!N_`h6up\38ۃo/Goq"ЇWY/T|x3EӇWԳ]O7Id$vz)'ivy;N{uB5ۥmյI%7q6(1/gܴ:ߏ-y8Ԣ2:c\~&GM}O͊fX9Wo<5h>IenjC" %0Xq3{9θrocj˧! FzaabrՋ$RՇs/>^.N19zf˟xhfpj:Kc&8{~p_ϕ?L5cV_'DF8 G,9hp55_xVNM8>8{r rX>w5i`ՏXuE0n6g~qe6jv篗bx3}!{<8l̩=F^8XL"B B)o` A) "WW3p7:q%>w'3?|x|pWX$1 :Y`08|S0(8RAհ]nprc7/gװz~J#sҫriQ_o0& Lq;.>3t—_5I~y©]~j'Fό^}&Et&]uSҿ%@ Txx{B|rwC'/ V\.-/ x/8|+>;==4[\M/\3c}h -ah%y✾/90Xϐb$G=Z Yu}F  zAiH1>MLB`X\vyx7DwV?Sc? }ʭ/Z|i͏ѹo;xXobU[oĿɁa~g5pѩb7O(.flh=Z~yxf$`{K@ 7/_~Da3w$ƙP[M;=rݝipg~3Xl=(×gp8èK8xx Kx.0tKA'N~ܖ<=a⨎ziF{#>? mra -ot!*v>` Et.&N}OdcO\b-aq1[ H879b]~aØݾ%mθҢ/p؛3#ng/hwzNꔏ; ^F'_;f{ա`+E:WfS(A WT|CCS5"լaw8 n\M{{ rҟ]_'W99:yYeVVyaP8쬦\yx`?w$40ŝY YAxg98 hWNu5ߠq2g5ph=ρf@iUFnf셀>Յi G=lUptvWt͑O]y!< (Fb8g,]H Ϯw+' È3w| y@5ć[nx\V%GzPr~twMCv|b$>hbա~-zMWqxqYͻ~ <ꃮxՍlIpae@#rgbL\E!YуIx͉d0ՇQSo zgo/Ǚ^9ǯ]L33.C߹8ݙ3tG,_=LЫ{3FKիK*ҠRHsc 2Xg z8|Yx>b[|j$nɟF {Uܸ{Qĭ=鯞8]|j屴W?tk|zXwύ:u ]铯Fu"!pG3r-~ Ë1DseƁӷx\v@ġ@Ԫ7Xzq7J=o`rs@ [_=.+\zѦ@rZaՃ|s>g6/>0}Hp{ҍ>+(`%tWpj(@<~V<8|5@e5_{R iǻr`kŃ5B"/m^ik>|vFOaCrgE..Zv ]>q ؆X3jZ& h8sk!0KH7:DMܽp4Cm5/X? ^^K|N[옾Ԗ =¨ϝ_pq6?zr%vtF#DatC<!'N|P[48>BrWbsQ1\0R/$jguc5 Ǫ_n\<_."^Ά G[3j&zHmF} FW4ORk['K+>69f^:iRG^ܪ}但{0%*;bI؝Ɔ%RO3p^Ν_=XyL<}A`4+@>^N1/,~u{jۙ3Nu%jɳfOS.άT\,*pWSz>|qM|~yOq`5_{/aE̽b<4YU\Xo8p&-uzhd84|t2^^X~1 z%Tf&^<>r3LZ LKWq{gw?2q 5riC"r Ao$>@#S1gq<POK¨'_ "7sog̗^uӃCj`p𷇓Wn?jFZңEVoo&L_wN~, 3_q>qйyc1.AHv"{}G ?nװB=o}m cVjWY2;xev>\6^&GC:=c^/Xafϙ>)_.]Xy|pͻwH|:hIdl>(M`9|k]5^38azy J^k FE ^XYi,̧^{Woj9M|w\A͋\k: P3>Ĵ 5 ;:\X3^~|=|bt}Яs OT]x9:ʥyX~}J߂K~W%b_rpm:";pwD0Ng;"| 'ƒSܰa{0p=j2maɧ&ipIEAgW6˷/OOŧF=+FL?4<~ɥ7690g7>~˭..Z\᪏(nח~ Y$5nF`+@|w J0K.4qiKG^p'݃a^p߱˅aՀߖ0;8fᇓtb8=3>/8yz\Y_θYza~p6\ +&YR;7XQOęj;YY>bogt0䚅:W>,뽞X.LIiow<"N{x7Yuf4oBf'QP;pgra5n 7=8NKS'p;QgCoJfq4p7vܬ~3>|̎vY~}wG}kg9땯so&[5K~H|giU Xq5ӧFfAy1`85*>]~7WLqzJp,xpދ9az0xp[<ԑ'և!r`|> #ytg˥6wƹ\3p;xIl4`=qKnbp+kp5D ^:iËӀcv=ŭ>3^bOww [ ٢!-̍9wW]~zB.4'pʇsM"ܑ; 7|,d ق/n_ϟ>~w*\qX||j--gdv|0w1gKa~QC-.™3>wg:K3=biRc@ ֒gS%YtႯ?eDWSlՠZ5 ǹ!!P>G.7%tl IDAT_׏ڰx[ nwaFw/h!]Of8:Ԑy9]:?f~CC,AI\!b$H.ЂIXq' +N37$1a0{{vw<g[-|/ O&,ȸ쌋!%T,YIx0곞3= ¥Xs-fWOɢ7]ܙWaA|\58˟PA֙?٠!v>j/.8Ѵ8gr@8S'/7rz}WKNjoы= C?dH%'907++Y<C\,L_qVMgzU7nċ Msq 4QЈ^J<x; G,XLcX> >å]=BKRAŁ9XxĄ&Y [vMQNyb: `Q?a+=Ւw>}oocEЁ Yfq󻗺9+oχ_ 7HCN"#WX!VqȻhw|j#'^s A;iSg_`Q]fzQSw"ȅ :vaͨC x3isu!&5^|rZ~83JbR@x{ 9˷<A>j/J:r~ ㌛.a-.6i5'~,/r(Ϝ!-LZj˒0/YN0>ݢƙnS_8NVoEX yjUW+K~}!ĩ7J]4Ի3mȯ_ش8#ZAF3K+$* Ol#P28g8>4Ƶqb԰YLoE̝mN8:[zC9TWնUgNӇzU}577>\g~wh$ߒ cx(܂q&&v{pc(8Ii>rԱn7ߚiҁ[vr5K8}A_o 3L>X|t8շ7b3ugufgj^\O3n:v30_p(.Y$֘TU b ǟx\10kex:rêϿ{4E4m-xzhn^<^;^Xzi2 z '/ >s`艻guo@rpȟyp +n&X"pdg j|8!h 6X~x1hBCg8We 94_ ֭S, bPqV2zXs9B\|aqЯ>PG5ic|;01" `E#!QC(ǯQ݃g {4i%S+G8g:|8{F-G" k|;>1&O7vwxZ4,}:(.ʙνdV׽zvklKLr,jaEy8>8;^8hk.0\|.3.sw+oIB+$"^#B\.'f F/GA!fլNb]oiS yՔP;*` >\ӋV\Sgo4KfC=sWS($H1I3xʫX|p>$8D]3j5PC4$Q/_-v9᪙zꛅVxo`t0ڙ^VtfwX=Z0:lY'^mqˤ>;>dX'=ԧ^-61/l1@܃w2˹̏'<LX= r^皃qO'F?L-w|8-;߿1.<{hh^~ 30rn9v<ѡ]L?_neLJ|>yȋi7p((рDbv1{Eq%@q1X/Agaԗg:Yݒiwn<|7gN o%9bꈻ˃ciW 'b4.30PE\_|7Maew#>w&]=0|?|a#4#9҆!_`+nhBI90{qw4/Fs&uOqqoX|zd0sA*.֢˄&zw9zL=oC?vȈV'!05p0јs[4~yzXz,9|ڝ頛61z8ózN/|gv}aq?yGPC;#08"3q!T3,&QQ*zʷi#ѓ{ Wxp[b0}S\Zp]/a`zb=+8?Ih=Z>y jPa83D|Pa581Jp-Tq|b9|8"ȣ/O{>+԰`haf_}2>K.wA/4 `GAX0'ڎO^bcd[VCMꤓOu!X_rruW߽bLo͋զ{˃a8rè/Kuqf}{`3twC cDHH8\cD֐%O|eˇu1;t|nuуW]?:`;_/Z\C.jZbv~?K|1_LNOU-g9+Y\Clo,Y#sGn .v,0,s>Nar uSL-+fgiƏ|i (n\ӫ,&ˇP`c>ZMgp +oíj,ŇI<".?==s(+CgOxqd{(8劋KK x[ۯޮâFm'6+)_ tK8p/^/WGs >-}Ś\d7X|8Ě%m|jnj"@dm *2B5D3 ey@SO^p(_aWCN<hê6i\j>hgՆ թ}kᨿf^`|ͭyipbDt6>9C1|WXs|' LB55v+5.c˅pw_wM8ᰊ>hY׻x3ӽrX_gсG߸hHʓcy|g~Cgyo iDJ+AWfvb<\||D4D]-41 sk0zp q`fb1^43p)~؝:ś63q7瞍~f. so5{G{Z丫WN9 " 4)hkm[]`ՁO~qӬ f|:mJSƛf;^\bf<g0Hv@?5A`?՘s i| 1b$ë%n|媛Nu< Yxo7`-߼ŋɧC^T^~90Sݢ9qVoׁu#Gl,9iq&GLvYu10}8-:; N$ +(ƏH,fx+g9 RK|>~ZM:5[zjݫLJc/ÀxC|-qE-,KOx13?gx{0b\h^|x+p5PS`, ÃCsa{|r!9+54"l֤=hjɍOU_pߝ&:ͤxsij"WOjfԩ_-f~qzaĘ{\,Ց+YX/'_M$y " [&: Y|qw*Wm >=壩!wά>pZIݽ)wS\o?_pgK̯s={<8zvr֫zvqۚjr _"!#zx>L C-530Mt Ǽ Fm Y.G#$X- |H#}08ӷূav|SZz L?ppvy4Y9{ȷ3<}{iԦ`7?xic>G맟~\G%z鳗'V|^xˆ/n S{zS%N aW"@ôUsN N-ZR/|錗>8E+brQ~X>/2Ћ;8p_߮gLY2r8YтG&CE>^5+~XV|t3z+^]\i9rڷLL\5kxl0guiͯf3Ϫ ~~< k,ow庫kyI O?n=<o'Z`‰#\É[i/hGhWigfĚ߾!06>bOodQ̋#X/f/\1-q&-vimp>恣%MqW{}⫆T6|8{hlg㦱 O>}4,$0~&_=$سÜ`P4b%P7 -Ca ïr{y*뙯efѝi61= .m^b}[掏W. )`P)nؙPyۻǗ1< BC7^XUCMgu$$$`MCm cW͢5IDAT`,x1{jHsK ` Z^AhpL/o4[6|AZtn^x1N}әj6McoNiUCm >;?U3"1db ӝXj^\MUC^56w5߄^jv;fi1?{vtú[4ȃWonC .wrN/O2qѭg>H;.w"!a0o{ V1w$C)ਆ!qe`:m>vjtpwXCT' N27y0b>Xry5?qWx>X߆6Wx|?0 oj?ECP+5I\ j}"Xjdrp8Mj%̏ o'~pcsҐ-xLqlL`,:~z₧B/Zpx]`sTU~V yx7DܪA4`j@\,kPD0pbծ~z.i5}n}ݞ.t)|~ugV`iKAW/o zկ;>3sࣟSϬգ¯~yp9v,^KۼX@V$GHF5z C.;N[=lC\x/Z k0L}~xO| M,Zװ8`,<ū-O̞n~zf%C3>P+ .Oue[fFq~tXeÉH" W.OI\X%V̎UF| ql1H~Fv .aԭ1 7|/f[W<^->5y4sy8;ղg" c11sGSώ׼z1`[bvzˁI~p Yd%2Bq~x\Q\~h_ θ҂2ÇH<XKj6N;~wu'Vq7zQ<.SW4Κ3hܗBO ewxפdg8'OAd*{` D!_DOģKRYbr3xjn1za-gj|-Z=ǧ4n1yΰrUa^ qlp0|4g>/io'`+x~+< w5{Dj:.ZgͧCtO<5S1W9Q顾p/'zetӤpCS8;입z(^M=tN/_xw|45$19;0xz(8vu6jw|˝nGga˙ee~p!7FC9V &N>m Osb˓>a%!kLCL=~!8nѣ`m>lI',O:⌷":՛8v].N3v^K>L9(_.k=H;E.a?l}4?mnԇe8ę*3^+Ǟzœ뉐 6O8+(~ "^̎S}m\srh![]wy}I] \gnx@mj\j~zԔYӏй`-&W-zap[`l,,<w9pcot)Yio,B_vKC O!^ϰapy`≦U RddiiҦ QafW?'~pv 7Ng/.gKܞL%'ro0w߯{ܫI>B `~@;#{&>΄0\r W.ѸaٙO>1;Ʉ /&N}jۭzLO.G^\j#Q?h҃ ۼ_Xp3nՇ+&nvLճR&C{c'9)nH=|>DHaf=`0|D-WL04YZLm8xgx(4W%;.L-=Z0u-_a8{a%3KK93/' W+._'̞^wb:#[Ȁ%'LLQb4hi;N+&Lsw7|yh<_-Ϗ.~;?<.G? /.|L OrX4ygrh cnb0,<=$ ӏK= Nsp뉏08O>z^?!0! Ha3_o9kL3qt/^C#G3nq1| ^z= ؆h~SϸS QbK3\/rG.o]b;/su//-}yrf<_G\@`DxSb 1vy|an@>rW=!qWXwyx,q| y+q]‘&+"K 'mjAOp=#Woa:|_<`k'.fIJN22bS0aH5rp 1X|0a_!w0q殞{x; ^?,b;Q/i·ٛI-Cn_߬7-^rWO}|{Hԙ [ [ͧp<+Nsè<pv 呶^^&Y5k0 Q. M6NXxY3SxGsq3X9訮{ZWOw/ϫm}#G;O+φ_brtIENDB`slixmpp-1.2.2/docs/_static/sphinxdoc.css0000644000175000001440000001370312473141653021275 0ustar mathieuiusers00000000000000/* * sphinxdoc.css_t * ~~~~~~~~~~~~~~~ * * Sphinx stylesheet -- sphinxdoc theme. Originally created by * Armin Ronacher for Werkzeug. * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; font-size: 14px; letter-spacing: -0.01em; line-height: 150%; text-align: center; background-color: #BFD1D4; color: black; padding: 0; border: 1px solid #aaa; margin: 0px 80px 0px 80px; min-width: 740px; } div.document { background-color: white; text-align: left; background-image: url(contents.png); background-repeat: repeat-x; } div.bodywrapper { margin: 0 240px 0 0; border-right: 1px solid #ccc; } div.body { margin: 0; padding: 0.5em 20px 20px 20px; } div.related { font-size: 1em; } div.related ul { background-image: url(navigation.png); height: 2em; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; } div.related ul li { margin: 0; padding: 0; height: 2em; float: left; } div.related ul li.right { float: right; margin-right: 5px; } div.related ul li a { margin: 0; padding: 0 5px 0 5px; line-height: 1.75em; color: #EE9816; } div.related ul li a:hover { color: #3CA8E7; } div.sphinxsidebarwrapper { padding: 0; } div.sphinxsidebar { margin: 0; padding: 0.5em 15px 15px 0; width: 210px; float: right; font-size: 1em; text-align: left; } div.sphinxsidebar h3, div.sphinxsidebar h4 { margin: 1em 0 0.5em 0; font-size: 1em; padding: 0.1em 0 0.1em 0.5em; color: white; border: 1px solid #86989B; background-color: #AFC1C4; } div.sphinxsidebar h3 a { color: white; } div.sphinxsidebar ul { padding-left: 1.5em; margin-top: 7px; padding: 0; line-height: 130%; } div.sphinxsidebar ul ul { margin-left: 20px; } div.footer { background-color: #E3EFF1; color: #86989B; padding: 3px 8px 3px 0; clear: both; font-size: 0.8em; text-align: right; } div.footer a { color: #86989B; text-decoration: underline; } /* -- body styles ----------------------------------------------------------- */ p { margin: 0.8em 0 0.5em 0; } a { color: #CA7900; text-decoration: none; } a:hover { color: #2491CF; } div.body a { text-decoration: underline; } h1 { margin: 0; padding: 0.7em 0 0.3em 0; font-size: 1.5em; color: #11557C; } h2 { margin: 1.3em 0 0.2em 0; font-size: 1.35em; padding: 0; } h3 { margin: 1em 0 -0.3em 0; font-size: 1.2em; } div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { color: black!important; } h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { display: none; margin: 0 0 0 0.3em; padding: 0 0.2em 0 0.2em; color: #aaa!important; } h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { display: inline; } h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, h5 a.anchor:hover, h6 a.anchor:hover { color: #777; background-color: #eee; } a.headerlink { color: #c60f0f!important; font-size: 1em; margin-left: 6px; padding: 0 4px 0 4px; text-decoration: none!important; } a.headerlink:hover { background-color: #ccc; color: white!important; } cite, code, tt { font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.95em; letter-spacing: 0.01em; } tt { background-color: #f2f2f2; border-bottom: 1px solid #ddd; color: #333; } tt.descname, tt.descclassname, tt.xref { border: 0; } hr { border: 1px solid #abc; margin: 2em; } a tt { border: 0; color: #CA7900; } a tt:hover { color: #2491CF; } pre { font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.95em; letter-spacing: 0.015em; line-height: 120%; padding: 0.5em; border: 1px solid #ccc; background-color: #f8f8f8; } pre a { color: inherit; text-decoration: underline; } td.linenos pre { padding: 0.5em 0; } div.quotebar { background-color: #f8f8f8; max-width: 250px; float: right; padding: 2px 7px; border: 1px solid #ccc; } div.topic { background-color: #f8f8f8; } table { border-collapse: collapse; margin: 0 -0.5em 0 -0.5em; } table td, table th { padding: 0.2em 0.5em 0.2em 0.5em; } div.admonition, div.warning { font-size: 0.9em; margin: 1em 0 1em 0; border: 1px solid #86989B; background-color: #f7f7f7; padding: 0; } div.admonition p, div.warning p { margin: 0.5em 1em 0.5em 1em; padding: 0; } div.admonition pre, div.warning pre { margin: 0.4em 1em 0.4em 1em; } div.admonition p.admonition-title, div.warning p.admonition-title { margin: 0; padding: 0.1em 0 0.1em 0.5em; color: white; border-bottom: 1px solid #86989B; font-weight: bold; background-color: #AFC1C4; } div.warning { border: 1px solid #940000; } div.warning p.admonition-title { background-color: #CF0000; border-bottom-color: #940000; } div.admonition ul, div.admonition ol, div.warning ul, div.warning ol { margin: 0.1em 0.5em 0.5em 3em; padding: 0; } div.versioninfo { margin: 1em 0 0 0; border: 1px solid #ccc; background-color: #DDEAF0; padding: 8px; line-height: 1.3em; font-size: 0.9em; } .viewcode-back { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; } div.viewcode-block:target { background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; } slixmpp-1.2.2/docs/_static/default.css0000644000175000001440000000771012473141653020723 0ustar mathieuiusers00000000000000/* * default.css_t * ~~~~~~~~~~~~~ * * Sphinx stylesheet -- default theme. * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: sans-serif; font-size: 100%; background-color: #11303d; color: #000; margin: 0; padding: 0; } div.document { background-color: #1c4e63; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } div.body { background-color: #ffffff; color: #000000; padding: 0 20px 30px 20px; } div.footer { color: #ffffff; width: 100%; padding: 9px 0 9px 0; text-align: center; font-size: 75%; } div.footer a { color: #ffffff; text-decoration: underline; } div.related { background-color: #133f52; line-height: 30px; color: #ffffff; } div.related a { color: #ffffff; } div.sphinxsidebar { } div.sphinxsidebar h3 { font-family: 'Trebuchet MS', sans-serif; color: #ffffff; font-size: 1.4em; font-weight: normal; margin: 0; padding: 0; } div.sphinxsidebar h3 a { color: #ffffff; } div.sphinxsidebar h4 { font-family: 'Trebuchet MS', sans-serif; color: #ffffff; font-size: 1.3em; font-weight: normal; margin: 5px 0 0 0; padding: 0; } div.sphinxsidebar p { color: #ffffff; } div.sphinxsidebar p.topless { margin: 5px 10px 10px 10px; } div.sphinxsidebar ul { margin: 10px; padding: 0; color: #ffffff; } div.sphinxsidebar a { color: #98dbcc; } div.sphinxsidebar input { border: 1px solid #98dbcc; font-family: sans-serif; font-size: 1em; } /* -- hyperlink styles ------------------------------------------------------ */ a { color: #355f7c; text-decoration: none; } a:visited { color: #355f7c; text-decoration: none; } a:hover { text-decoration: underline; } /* -- body styles ----------------------------------------------------------- */ div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Trebuchet MS', sans-serif; background-color: #f2f2f2; font-weight: normal; color: #20435c; border-bottom: 1px solid #ccc; margin: 20px -20px 10px -20px; padding: 3px 0 3px 10px; } div.body h1 { margin-top: 0; font-size: 200%; } div.body h2 { font-size: 160%; } div.body h3 { font-size: 140%; } div.body h4 { font-size: 120%; } div.body h5 { font-size: 110%; } div.body h6 { font-size: 100%; } a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { text-align: justify; line-height: 130%; } div.admonition p.admonition-title + p { display: inline; } div.admonition p { margin-bottom: 5px; } div.admonition pre { margin-bottom: 5px; } div.admonition ul, div.admonition ol { margin-bottom: 5px; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre { padding: 5px; background-color: #eeffcc; color: #333333; line-height: 120%; border: 1px solid #ac9; border-left: none; border-right: none; } tt { background-color: #ecf0f3; padding: 0 1px 0 1px; font-size: 0.95em; } th { background-color: #ede; } .warning tt { background: #efc2c2; } .note tt { background: #d6d6d6; } .viewcode-back { font-family: sans-serif; } div.viewcode-block:target { background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; }slixmpp-1.2.2/docs/_static/agogo.css0000644000175000001440000001530212473141653020367 0ustar mathieuiusers00000000000000/* * agogo.css_t * ~~~~~~~~~~~ * * Sphinx stylesheet -- agogo theme. * * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ * { margin: 0px; padding: 0px; } body { font-family: "Verdana", Arial, sans-serif; line-height: 1.4em; color: black; background-color: #eeeeec; } /* Page layout */ div.header, div.content, div.footer { width: 70em; margin-left: auto; margin-right: auto; } div.header-wrapper { background: url(bgtop.png) top left repeat-x; border-bottom: 3px solid #2e3436; } /* Default body styles */ a { color: #ce5c00; } div.bodywrapper a, div.footer a { text-decoration: underline; } .clearer { clear: both; } .left { float: left; } .right { float: right; } .line-block { display: block; margin-top: 1em; margin-bottom: 1em; } .line-block .line-block { margin-top: 0; margin-bottom: 0; margin-left: 1.5em; } h1, h2, h3, h4 { font-family: "Georgia", "Times New Roman", serif; font-weight: normal; color: #3465a4; margin-bottom: .8em; } h1 { color: #204a87; } h2 { padding-bottom: .5em; border-bottom: 1px solid #3465a4; } a.headerlink { visibility: hidden; color: #dddddd; padding-left: .3em; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } img { border: 0; } div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 2px 7px 1px 7px; border-left: 0.2em solid black; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; } dt:target, .highlighted { background-color: #fbe54e; } /* Header */ div.header { padding-top: 10px; padding-bottom: 10px; } div.header h1 { font-family: "Georgia", "Times New Roman", serif; font-weight: normal; font-size: 180%; letter-spacing: .08em; } div.header h1 a { color: white; } div.header div.rel { margin-top: 1em; } div.header div.rel a { color: #fcaf3e; letter-spacing: .1em; text-transform: uppercase; } p.logo { float: right; } img.logo { border: 0; } /* Content */ div.content-wrapper { background-color: white; padding-top: 20px; padding-bottom: 20px; } div.document { width: 50em; float: left; } div.body { padding-right: 2em; text-align: justify; } div.document ul { margin: 1.5em; list-style-type: square; } div.document dd { margin-left: 1.2em; margin-top: .4em; margin-bottom: 1em; } div.document .section { margin-top: 1.7em; } div.document .section:first-child { margin-top: 0px; } div.document div.highlight { padding: 3px; background-color: #eeeeec; border-top: 2px solid #dddddd; border-bottom: 2px solid #dddddd; margin-top: .8em; margin-bottom: .8em; } div.document h2 { margin-top: .7em; } div.document p { margin-bottom: .5em; } div.document li.toctree-l1 { margin-bottom: 1em; } div.document .descname { font-weight: bold; } div.document .docutils.literal { background-color: #eeeeec; padding: 1px; } div.document .docutils.xref.literal { background-color: transparent; padding: 0px; } div.document blockquote { margin: 1em; } div.document ol { margin: 1.5em; } /* Sidebar */ div.sidebar { width: 20em; float: right; font-size: .9em; } div.sidebar a, div.header a { text-decoration: none; } div.sidebar a:hover, div.header a:hover { text-decoration: underline; } div.sidebar h3 { color: #2e3436; text-transform: uppercase; font-size: 130%; letter-spacing: .1em; } div.sidebar ul { list-style-type: none; } div.sidebar li.toctree-l1 a { display: block; padding: 1px; border: 1px solid #dddddd; background-color: #eeeeec; margin-bottom: .4em; padding-left: 3px; color: #2e3436; } div.sidebar li.toctree-l2 a { background-color: transparent; border: none; margin-left: 1em; border-bottom: 1px solid #dddddd; } div.sidebar li.toctree-l3 a { background-color: transparent; border: none; margin-left: 2em; border-bottom: 1px solid #dddddd; } div.sidebar li.toctree-l2:last-child a { border-bottom: none; } div.sidebar li.toctree-l1.current a { border-right: 5px solid #fcaf3e; } div.sidebar li.toctree-l1.current li.toctree-l2 a { border-right: none; } /* Footer */ div.footer-wrapper { background: url(bgfooter.png) top left repeat-x; border-top: 4px solid #babdb6; padding-top: 10px; padding-bottom: 10px; min-height: 80px; } div.footer, div.footer a { color: #888a85; } div.footer .right { text-align: right; } div.footer .left { text-transform: uppercase; } /* Styles copied from basic theme */ img.align-left, .figure.align-left, object.align-left { clear: left; float: left; margin-right: 1em; } img.align-right, .figure.align-right, object.align-right { clear: right; float: right; margin-left: 1em; } img.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } .align-left { text-align: left; } .align-center { clear: both; text-align: center; } .align-right { text-align: right; } /* -- search page ----------------------------------------------------------- */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* -- index page ------------------------------------------------------------ */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* -- general index --------------------------------------------------------- */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #f2f2f2; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } /* -- viewcode extension ---------------------------------------------------- */ .viewcode-link { float: right; } .viewcode-back { float: right; font-family:: "Verdana", Arial, sans-serif; } div.viewcode-block:target { margin: -1px -3px; padding: 0 3px; background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; }slixmpp-1.2.2/docs/_static/fonts/0000755000175000001440000000000013014656513017707 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/_static/fonts/YanoneKaffeesatz-Thin.ttf0000644000175000001440000037404412473141653024601 0ustar mathieuiusers00000000000000GPOSq|GSUB@Q'DOS/2S``cmap C cvt hfpgmY7 sgaspg qlglyf]_sheadc6hheaET$hmtx'kern loca+- maxpx namet1[\|#post'nprepƝ8]sH _< |%`"z8 89zUi s!@ KYN @ 89  "  .MJ ,t''I'03,0; ***4/+-:.*:10(: @l-B_CSC-@@*@S@@@-z@-@{'j@z f  z*BZ//-)?;BBgB@%B.@//!En3}%{<b+)%)?s3*      l-_C_C_C_C-@-----e0@@@@f:z*z*z*z*z*z**Z/----0@%%%%%0&EEEE}}B-.{'/f%?.0))))))<3J % 3z'&0  py!343?sS JJ==JsJpJ`03A%=% +~l8'3}=0E/C&&Y9e06!= )+B9t99/Bz@<'n&   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYjkmov{[`^a]l}\eZ_dfizbhpgqnsturxyw~cHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcdHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcd,K PXYD _^-, EiD`-,*!-, F%FRX#Y Id F had%F hadRX#eY/ SXi TX!@Yi TX!@eYY:-, F%FRX#Y F jad%F jadRX#Y/-,K &PXQXD@DY!! EPXD!YY-, EiD` E}iD`-,*-,K &SX@Y &SX#!#Y &SX#!#Y &SX#!#Y &SX#!@#Y &SX%EPX#!#!%E#!#!Y!YD- ,KSXED!!Y-+++kQ7+qQ7++ E}iDK`RXY! B    .k+// /EX/ >YEX / >Y ++ и ии!и%и'и,и.01%#7##7#46;7#46;7>337>33+3#'37#4  G=PF  OFXXcXc  Q\Q\  Ms;d7+;/EX/>YEX/>YEX7/7>Y*+ и/01".546732>54&/.54675463.#"#"0 30$3"*p %GA *  8+7C"}*>K   !/#>'b8!/<f w '-1m&B*@Vc J/?P(8+0 +++EX/ >YEXE/E >YEXG/G >YEX+/+>YEX@/@>YEXB/B>Y+5014632#".732654.#"4632#".732654.#""'&50JLB 2"HA 3$! &3:%2;LB 2"HA 3$! &3:%2; %>N)` aX*G6\[ 'H6-:" NN.<"HQaX*G6\[ 'H6-:" NN.<"H=c}B,Qa˸b//!и!//4и4/bEиE/ZMиM/EX/ >YEX/ >YEX!/! >YEX$/$ >YEX:/:>YEX@/@>Y $)*и@_012&#">54.'>3:#'#"&'#".54>7.54>.'3264%2Q0&4P_, &-2\ ,  - N<9G(!*%0.<+b*$ 9-5C  !,#%myy1,' (G;,   3H. %,1+)#!,!4B 9/% Y01#".54>32k&58 9@3&.(93#DhS F}QkP5 Lq9&" +/EX/>Y014.'>32#"'>$399B63@885'D`qM Y0174632#"&'>'.A  "2* 00 * +0146;#0   ;|A "+EX / >Y0174632#"&;  //01#>30   *X P//и/EX / >Y+ 014632#"&732654.#"*We1B(Sc^T"GHSC 5(TG($KqL|CdB!*Y*Y%+ /EX/>YEX/>Yи/$ &01.50>3233:727#!4&<50>2623"T%+& e $+&.F   *X+R $+EX/>Y)+)9ии/"901&54>32;27#!.5467>54.#"E-?),7 >Ze'0 #_W=(!?T  !/?mii:  7ccg;"4X:7+ +1&++и/01#"&546732>54./>54.#".54>327*;S4:82(*E1/= AQ(!?M,@)+7 $.0#7)6]D' #=R/(. (X2    *3-'/["V+и /EX / >Y+ ии01%3+#5#&54>7>327_V'@T- $=:/    fvv)x`O\f3+X*C!+и/+*+ &+и/01+"'2636#"&54732>54.#"7F%*ND/5YB;<8)7K-)YEX/>Y'+/01">32#".54>32."32654&$D5"Y9":*\c6B$ %@S/(  54V 6-NQE)TW680J3w,NmAqc, A<3]G*raYR:X//+01"&5>7*"#"5+UNCCIK!1Pg9 Xи>/EX/ >YEX/>Y9)993D39012#"&54>7.'.54>.'32654&">54.;3"&.#'VX^\&3!# 0?q*5(&9'GI)j3(& )1%)1 "@4!5*  C9YdiT">3(  C$+?(x #0:!:*WF1> 3# 8  '.+4*b,e-/"/и/- и /" *и/*и/+'++01267#".54632#"&54672674.#"jnR;!;+\d5B$ {1#S0R7.NQMG38/I3}&KpJ  ?94`I,klYQ:{ >+ иEX/>Y +014632#"&4632#"&:1~ 5+ и /и/// +014632#"&4632#"&'>5&= 2* 00| ++01463!#463!#0SS[     :[,++$и$///EX / >YEX / >Yܸ 10174632#"&2"'&'.5467>54.#".54>a-2(')   (%"% @+ ( 8,*F:1, 205>&'-!  :Di| %+-+^L+r?+LGиG/LIиI/Ljиj/EX/>YEX4/4>YEX:/:>Y*+[Q+Dm+4ew014.#"32>7#".54>32#"&'#.7>3265<76.#"&547>3232>'.#"32>5AfHGvU/'8DN+.H8+"?Y6.VL?-4_N%$! 8./#-<#  $#8 9.9< +,!"1&'^PtK#)]n`]8   ;djuc,SuM,  ;00=$  %   4D$+ @$ 3('0   EX / >YEX / >YEX / >YEX/>YEX/>YEX/>YEX/>YEX/>Y+ 01%##*'432#*'%3'.'O6   =    M !?5( (5?!@y&3o4/-/4и/-&'EX/ >YEX/>Y3+%'0146;2+32>54.+32>54.+@ d;1 #*&-?Bw!a.#&1YU9-,:"T !;0.=$ 9/T`0 !9,%1 .O>.5-P3C-+EX/ >YEX&/&>Y &012.#"32>7#".54>" &# '/$"-&%!&*.   1RxS^p<    )GlNZX5 BY// и /EX/ >YEX / >Y 012+4634&+32>0H1!8I'v OUXU f+EX/ >YEX / >YEX/>Y+  01%+476;+3+>    C=m+ EX/ >YEX/ >YEX/>YEX/>Y + 013476;+3+#C  -m,g-//-(и(/ EX/ >YEX#/#>Y+#012.#"32>7#54;#".54>,<, 'A0*8#u 0"(D17L JflC  LqpQ@}/ /ии/ и EX / >YEX/ >YEX/>YEX/>Y +01!#463!463#b   p!O@a/+EX/ >YEX/>Y01463#@  O5+EX/ >YEX / >Y01463#"&54732>5/*,4 *#@5" -:@y{+EX/ >YEX/ >YEX/ >YEX/>YEX/>YEX/>Y014636232#*'.'.'#@  B|2 1.* -$NnL4 @? 5+EX / >YEX/>Y0173+463c  @;YEX/ >YEX/>YEX;/;>Y014;67>7>;#<6465+'4.'#@ x   w =<   y)"!' OG /9:66<;0 -9;6SQ?@&'//'и/и/и/#&и&/EX/ >YEX"/" >YEX/>YEX%/%>Y01.'#4;463+!   ~"  '=KJAFQRD, +@MJ?cO -'](//(и/ EX/ >YEX/>Y#014>32#".732>54.#"-0H10D*-H10D-##8'(;%#6&(;'c`U'%TbcW($U[XyL"#P]ZzK #P@^]// и / EX/ >YEX / >Y+012+#434.+326/@'\RO 4'TOHD2C%jh 7*`-~#7|8/./8и/. $EX/ >YEX/>YEX/>Y+)3014>32327#"&'.732>54.#"-0H10D*'<* + '. .C+##8'(;%#6&(;'c`U'%Tb\X.7&  6B&UYXyL"#P]ZzK #P@^#.//$//и/!и!/*EX/ >YEX!/! >YEX/>YEX/>Y,+)*012#.'"+#47634.+326/@'=8 !  ! O 4'TOHD2C%Vd 2Ld>HlK,8*`'W1}2/&/2/и// &/и/EX/ >YEX/>Y##9+9012.#"#".54732654./.546?; 9-9E$*A-#1!52IAr"'V  5,"6{)*,'B/  N>*((m#B'9Hd A+EX / >YEX/>Y и01##543!# T n  @`//и/ EX / >YEX/ >YEX/>Y0173267463#".5463a ;3(L  #-3>I% 'I8![ &AT.GEX/ >YEX/ >YEX/>YEX/>Y01&4;>7>;#"&'.'& {   !0,+-U7< Yel4yv=iEX/ >YEX!/! >YEX#/#>YEX&/&>YEX1/1>YEX4/4>Y01&4;3>7>733>7>;#"&''#"&'.'&l  Ze  b  ` a    +&&/M-D*&%#U/<11&;  Yel3y %zEX/ >YEX/ >YEX"/" >YEX%/% >YEX/>YEX / >YEX/>Y01+/+.623>?>3 !OTP<8 k #.6nH92& &1:yab+EX/ >YEX/ >YEX/ >YEX/ >YEX/>Y01&634>?>3# 8  4 !QG11FQ"ap5EX/ >YEX/>Y0143!3267#!&547!7 ) &    {;52 +EX / >Y+ 01+3+463 5 B   //01#&63   8 62 +EX/>Y +0146;#46;#    q +01463!#SQ  R //010.54>30%#R *E(;YEX/>Y,+$60147>32#.54>3254.#"&.#"3267>*X69=!+4).:/#6P-2&!* 3  5D@V< :35@" _% 5.*/ &vBx&r'//'и/ EX/ >YEX/>YEX/>Y#01463>32#"'732>54.#"BA02$'+,M'! (#70 +$@+H5JkJ,YEX/>Y$01.#"3267#".54>329 *3*!.%4+ 7("5@  2aRDT.  5bRZk9 /^%i&//&и/и/EX"/" >YEX/>Y" 01.#"3267463#".54>32<#7(#24I+!<,1A$, ;Y=KW- 53dTFfB -](:;/6/и/;$и$/и/.и63и3/EX/ >YEX/>Y.+)012+3267#".54>":>7>5."/B>7$2$C$0$=-"4?1* 7CG #):#:BO(   0[Mcs; -ZO" 3#S#p+и"EX/ >YEX/ >YEX!/!>Y +ии#0146;54>32.#"3++ G/7-%#-%uj  B:E$  :2@ 6 ):ASij/e/#и/#иj-и-/[EX/ >YEX/ >YEX/ >YEX(/(>Y (`TܹܸBL0123#"&'#"'#".54>7.54>7.54>"32>54&&'32>54. ~&!''>+'#->$0%0I12=#   '  ',;!/#1*!/B2& 1((<()C3&F5 )   &1(&%#   T1-C-%9&5)/;C@1  " &?f}//и/ EX/ >YEX/>YEX/>YEX/>Y01463>32#4.#"#? !I"&0 &I$2r(a;j&<%EX/ >YEX/>Y01>q&C+EX/ >YEX/>Y 01Bn8C+4и>4/,/EX / >YEX/>YEX!/!>YEX8/8>Y, ;4?01463>326"#>54&'.#"#4#">B )*(0)(* +*"     ?!M%+L9" -,$7( 6) !- =#+ #,6Bc/+EX/>YEX/>Y017#463c  B+,  +'++EX/ >YEX/ >YEX / >YEX/>YEX+/+>Y$01.#"#632>32#4.#"+( 7 OC*/ F $, 9   F!&8$o* O@kh/ /и/  EX/ >YEX / >YEX/>Y01>32#4.#"#@&R,)5 (!$# $5"j, H%g!]"//"и/EX/ >YEX / >Y0174632#".732654.#"%ZM&:'VP#:)" .YEX/>YEX/>YEX / >Y#01>32#"&'#"532>54.#"B!D*'>+&7=$5 !33.!"2! 52S=fx@ 5hY:L,.:e"v#//# и /и EX/ >YEX/>YEX/>Y01+5#".54632.#"367e @+4.m[#5! -MS&,C  0\N ~DN) "*@9+EX/ >YEX/>Y 01>32.#"#@"D#( ;+    !N/U23/%/и/и/3.и./.и/.и/EX/ >YEX/>Y ""9( 9012.#"#"&546732654&/.54>!0  7$.##z%%UM>C5*;G"#p,;   $M;&BA   +0#5E)!"t +и  /EX/ >YEX / >YEX/>Y иии01#46;54633+3267#".5rQG  /!  /"  ,:#Ep\//и/EX/ >YEX/ >YEX/>Y01%#".54>233267463p%Q#,8!  /$A -I45,;# PGEX/ >YEX/ >YEX/>YEX/>Y01&63367>?>3"#"&'  5  /   2b'.**.'b3EiEX/ >YEX%/% >YEX'/'>YEX)/)>YEX9/9>YEX;/;>Y01&633>?6;3>?>23+"&/.'+"&'.'& 0   & /   * <<       3Z#($:CH 0U"("8EK$E1]N==O]1|@IL$T!zEX/ >YEX/ >YEX/ >YEX!/! >YEX/>YEX / >YEX/>Y01#'.'#'&63>?>23 @e9 =Q 8 ].( T Y{"(Q?["<EX/ >YEX/ >YEX/>Y01&6367>7>3#"&54673267*]  U C,% " 7 12G YF  @R%_!EX/ >YEX/ >YEX/>YEX/>Y к 9 и /9013267#!&'&547#"&4543Z6= -B  L  8/"+/EX/>Y01#463   <:} ;+/EX / >YEX/>Y 01#"&54632#>3}Z bnm5m,+&/5/EX/ >YEX1/1 >YEX!/!>YEX'/'>Y !0132.#"3267+#5.54>754635+"1/% 1#+6   6fVEO(   v 1ZJTk?{ +40+и0*EX / >YEX/ >YEX/ >YEX#/# >YEX4/4>Y+ + $и(и+и/017#46;5'#46;&>23>?>233+3+#ˍ }w \~7  4 }j d y I7 E!ME11DN! 5K p  +и 014632#"74632#" " )A%5GI6&+ +.>+;1+)C++#+01.#"3267#".54>324632#".732654.#"!    # !hZ-D.cX,F2'9%KZ'9#&>+2*!& 1*4< ui3U?lo0U@6G*``7I,/L%o$I%EX/ >YEX/>Y014>7672#&'.4>7672#&'.% !+  '(  -" !+  &'  -" $+4?!(0;>2*" B6-%"(0<&-77,% ;0(")A!<Ei+47+%=++%*и*/=-и-/4A ++C2+014632#".732654.#"72+.'##47634&+326)hZ-D.cX,F2'9%KZ'9#&>+0$/ $) q%!ui3U?lo0U@6G*``7I,/L]+; :0+o Ik J //01"&'&'>12 0((2J*! &?0 + +014632#"&?Js%++017272632#"'&'6767632654&#"7>     @   [3z$HGEX/ >YEX/ >YEX/>YEX / >Y01&'&54>767&'.546767&'&54>?&'.546767z",  ('  *! ", H'  *! %-6B !*2?;0(!?4+$ "(0; %d7-&<0(">  :W+,+$и$/EX / >YEX / >Y ܸ 101#"&54632".54>7>54&762327-2(')   (%"& >, )V 8,*F:1, 205>&'-   & >h & a & b v& L {& ]t & x$+иEX/ >YEX/ >YEX/>YEX/>YEX / >YEX / >Y++01%+5##*'6;+3+'3xh e      144-JPVhP+EX/ >YEX&/&>YEXK/K>Y?4+,E+ &012.#"32>7272632#"'&'6767632654&#"7.54>" &# '/$ +     4+&*.   1RxS^p<    3   OD~iZX5 C>&$>ZCC&$aC>&$UC>{&$]g{&(>-&(a-&({&(]@v&-f-&.>-&.a-&.z-v&.d-{&.]eW&'7'>76'Y bb Z[ dc fpqggqp0+<GH/,/H и /,и/,! =EX/ >YEX/ >YEX/ >YEX/>YEX/>YEX&/&>Y8C01"&'4>7.54>3272#"&'4&'32>%.#"[ /H0$8 .G1&:% %,/(!=(;'#.';' *m`U'4*icW("u[{%^pvfL*"NOI #P@&4>@&4a@&4@{&4]a&8a:NyA+2+++EX/ >YEX/>YEXE/E>YEXG/G>Y&>014>32#".546732>54.54>54.#"#"'&'67>5A(8#6H-!%!+2+SE+ /&+!+2+!%!$:)?= %+;%+1" #,8&HG   * 2)#$'!'!AP  *E&?>O*E&?a*E&?I*J&?3*E&?][*E&?k*XIZlR7+.Z+j+7и/. и /.и/&и&/Z?и.`и`/jgиg/EX/ >YEX/ >YEX+/+>YEX2/2>Y`J++ E WиE[иJeиe/Jgиg/01467>32>32#"&'3267#"&'#.54>3254.#"&.#"3267"327>5..J<6: #(,C:-:a$1%C%0/H C84).:/%/I-2&!*9 .) %-3#%  ,9"(RF?"DM(  ,&;35@# ^% 70(-  'OE'AD/J:Kh B+EXG/G >YEX/>YEX=/=>Y1&+7+G01.#"3267272632#"'&'6767632654&#"7.54>329 *3*!.%49/     1$"5@  2aRDT. 3   O6_MZk9 -]&C>i-]&Ca-]&Cc-]&C]u~&>0&a0&&]@k&LT%g&M>[%g&Ma%g&MU%g&M@%g&M]h0r7+и +++01463!#4632#"&4632#"&0S   u& )9DE/*/E и /и/*и/и/*и/ :%и%/EX/ >YEX/ >YEX/>YEX"/">Y7@01"&50>7.546327#"&'4&'326'.#";  XM$8& TK*? " $),&0 / on]0N8ZEp&S>mEp&SaEp&ShEp&S]z?[&Wa?[&W]bBe/+EX/ >YEX/>Y017#463e  -q&:;//; и /ии/&и '1и1/EX/ >YEX/ >YEX/>Y!$+01%+5#".54>325476;+3+32>54.#"qH80D,/H04D$7')8" 4&';'  `91$Uh`V',2I  6XyL"#P]ZzK!$O.4GY5+M>+U+и/Mи#и#/URиR/EX/ >YEX / >YEX(/(>YEX0/0>YM+(:иCH0174632>32+3267#".'#".7326754.#"%":>7>5..XM6H %+.D9B>7$1%C$00) F1&<*" .;F.1"0* 7CG $z2B).RF?DN(  +#756fOCV1ir EX38Z,[R' 2"'W&2R/U&Q[a{&8]Rp&9b%_&XO?X!9+EX/>YEX/>Y+014>32.#"#"'&'7>5F);$+ 5( .  K+:#   3'   K- //EX/ >YEX/ >Y01632#&'.'#"&'n0  0  O ///01".5>32>767{  ! O & .EX/ >Y01632#&!" G  ++014632#"&732654&#"0()'-*%,-)&0*+%. W$/++ и /!и!/01>3232>32#".#"#"'&,  p'   0 * +01463!#0S    * +01463!#   )Gi+ //01#"&54>32d    f:5$ K)?j +/ /014632#"&'>5..   95$"#! )j_+ //0174632#"&'>5..   ?:4% "#! )G'G(//(и/и/"и"/ ////01#"&54>32#"&54>32d        f:5$ K :5$ K)? +K,//и/,и/$и$/// /!/014632#"&'>5.'4632#"&'>5.      95$"#!  95$"#! )_+K,//и/,и/$и$/ /!///0174632#"&'>5.'4632#"&'>5.      ?:4% "#!  :4% "#! <O + +014632#"&<  3A&'J 0@RbtcS+[k+A1+9I+!+)+EX/ >YEX/ >YEX/ >YEX/>YEX/>YEXYEX^/^>Y4N+.<F4VиFhиNp01"'&504632#".732654.#"4632#".732654.#"4632#".732654.#" &=O)`LB 2"HA 3$! &3:%2;KB 2"HA 2$ &3:%)KB 2"HA 2$ &3:%) =c~BRaX*G6\[ 'H6-:" NN.<"HPaX*G6\[ 'H6-:" NN.<"&='aX*G6\[ 'H6-:" NN.<"&=%$%EX/ >YEX/>Y0174>7672#&'.% !+  '(  -" $,5A")1==0(! @5-$3$GEX/ >YEX/ >YEX/>YEX / >Y017&'.54>?&'.546767", H'  *! $-5@ !(n=1)"A5,$z\ GEX/ >YEX/ >YEX/>YEX/>Y01#"&'632h   ]G:+и/ и /:5и5/:>и>/:CиC/EX0/0>Y +++0%5и9и>иB012.#"3+3+3267#".'#46;5467#46;>4# /)!   '/ : *:0 ] K` R '29]   %H= 1 DO(  0[N 0 FT,&4F69+04++ии/0+и+//4/9/EX/ >YEX/ >YEX/ >YEX>/> >YEX@/@ >Y5:и;01476;7>?46;#5464651+'.'###5476;#8  6 %' ug   t%( `e/ %'i""0 * +01463!#0S   12?@/1/@и/и/ и1,%и,.и./3и14EX / >YEX&/& >YEX3/3 >YEX/>YEX/>YEX./.>YEX0/0>Y +3иии/*и+и2и :и:/01+#46;54>32>32.#"3++'3547.#"S G#15 8B)(#-$uj, '$6 M9@   :2@ 6BD'4. X,ٸ-//- и / %и (и(/ +EX/ >YEX/ >YEX/ >YEX / >YEX / >YEX/>YEX(/(>YEX*/*>Y $и%и,01%#46346;54>32&#"3++G  F/9)1'/-%aV  B=F#  :3? 6 P(Ѹ)//)и/ и/!и$и$/'EX/ >YEX/ >YEX/>YEX/>YEX$/$>YEX&/&>Y + и!и(0146;54>32+.#"3++  F,1:+!'"ti B=F!?:5A 6=2@H?+$)+AD+$и?+и1и)3EX/ >YEX/ >YEX?/? >YEXH/H >YEX&/&>YEX(/(>YEX./.>YEX0/0>YEXD/D>Y+ и /"и#и*и+и2и9и9/0146;54>32>32&#"3++#+7467.#"3#463G#248B*0'/-%aV +($  M9@   :3? 66a$54.K65C ',++B+' иB.и4и,6EX/ >YEX!/! >YEXB/B >YEX/>YEX)/)>YEX+/+>YEX1/1>YEX3/3>Y+ и /%и&и-и.и5и<и32>32#.#"3++#+7467.#"3G#246:%2!' aV +($ M9@ >73F 66a%5 4.K!&RX/38g%67//7и/!&и.EX/ >YEX/>YEX/>Y )301"&54732>=#".54>32.#"3267YEX/ >YEX / >YEX"/">Y +ии$0146;54>32.#"3++G$ !  uj ~%, "y 64Kc5+/K/:+"+/0+0и/и//,и,/GиG/017".5467".54>75463.#"326732>7#1E,D1$0@& &7$ L3!9*!,+6L0$9'&9)  %<- 6*:"AL -%<)l } #& 1!% #(4-#f 3vmx+i+ a++иBи HиH/iWиW/EX=/=>Yd+M\+=1014.#"26767>>7675463>76732767#".5#32>7#".54632=  /- &O &$/ ,( " &'! DD:8%1*!5%$=,Tg'! *5 /^SP (2  "@6  LS(  2^O !<0!?xJK/C/K и / и/ и/$иC0EX%/% >YEX/>YEX/>Y+@5+%01#"'&'7>54>32.#"!#".546732654&'.#7g *=&) 5( . Z ?4"2H.!+ ))NR4)77   +9"    5(C %=-1RY+01463!#".546732654&'.#7 ?5"1I.!+ ))NS5)8  %=-1RYEX/ >YEX/>Y,+и01##46;5463!#".546732654&'.#7lc W ?5"1H. +  NQ5)8=  %=-1RYEX / >Y +и и !012+#46;4634&+3+32>0H1!8I'v0% OUX|qU YEX / >YEX / >Y0173+.?463ckk.. ) )U 5 G +и  //EX/>YEX / >Y017#.?463c::::w I bJ!cA+EX / >YEX / >Y+01.547>3:3#!&54;R., g    }`Jl:H"2+EX/ >Y/%+8+и/014632>54.#"&67>32#"&'&54732654&'.#.)##*@ D4#/ $*\O-:  0,Y+(01.54>323#!.5467>54.#"U'4$. /EO! $QD-#/;r '(GDB#  &BAA% "={"e +и  / /EX/ >YEX/ >Y+ ии/013+#"'5#&547>7632)OI +4;  00(   DQ QQI YEX / >YEX"/" >YEX$/$ >YEX/>YEX/>YEX=/=>Y+.N+и=801.547>3:3#!&54;#"&'632&54>323#!.5467>54.#"R., g  (   '4$. /EO! $QD-#/;  }`\  '(GDB#  &BAA% "J(&'iJ%&'w`; q+ и////EX/>YEX / >YEX/>YEX/>Y01#*'43:#*'432    X  b 0{\ ///01#"'%&547%\  n p   3w_// /01%>32.5473   np  Aiy // /017#"&'7'd v=i;;$+ и$7++/*+01##".54654&'.547>54&54>  + %# .%6$$ *9 #'cA62 '-Df/'+ 3&6e?*"   ;c4", i:;3 + и3'++*.+01&543>54&5467.54654.'.5432 + %# .IG% *9 #'cA62 (,Df/'+9L348 *"   ;c4!-  >! ++01>3232676#".#"& &"!+ %&     + (+и/$EX/ >YEX / >YEX / >YEX/ >YEX/ >YEX!/!>Y и/ии/014632'6'.7&547#"&'46    0       .l: ?//и/ ++014632#".73254&#"l0)((.) :,*&0*+!: `S,\+и'EX / >Y"+(+ и(01&+.5#46;54>32.#"3# QF*9!*9( .!<}<SA Rh; 2[I 8IDXY/K/и/и/Y:и:/%и%/:'и'/:VEX/ >YEX"/">Y ",012.#"#"&'&54732654&'.'.5467&'.546>54&'.'/= 2%27 $)& "# LC0< 2%27 $)&  $K@"@  ' !!.!8 %-8  ' !!.!8%-8y2 3%2 %3#/++$)+EX/ >YEX/>YEX/>YEX&/&>YEX)/)>Y01%#"'5#".54632.#"367#*'43:j  ;!A3g`&3! * NT+68    5[F  z>O.  v!jEX/ >YEX/>YEX/>Y+ +и ии 017#"'7#46;7#46;76323+3#: 8uw29 7{}2      =}?$7LgM/0/Mи/08-+5+и и /-=и=/5F014>32>32&'.'#".%32654&'&"32>75.'"= 4)* '7BB;  "0  &!/4 $+7!"" "$ 4( 0)"#FEHI 6#$'3U !'8:&4 N')'; 0 I+ и//++и 01#5#46;54633#463!#  S    E>p !//! и /EX/ >YEX/ >YEX/ >YEX / >YEX / >YEX / >YEX/>Y01%#"'#"&'43:3267463:pQ(S" A3%;   -:0 /*+//+и/и/!EX/ >YEX/>YEX / >YEX/>Y&012.'632#"&5464'&#"32>(B\Q 6M3 %F9Y\\1VHO'6#09 T8&[nN:lS2~~~1-Uop>Y80NdCBZ9EX/ >YEX/>Y 01!+3#C̝  2&@//и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yиии01#"'##"'#.543!#T   ;]YY&//и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yиии01#"'##"'#.543!#T   ;]55=%2+EX/>Y + 014>32.#"#"&54732>5/8"/#-$.9#.#-$;=D  73>C  829! $5^6//6и/%и-EX/ >Y2 +(+01>32#".54>3254.#"..#"3267B<.% A*)"-*#8'&!-  $/('._&!" 0"5#P$//$и/ EX/ >Y +014>32#".732654.#"0$2/FA.! #18 "'9P30R?jf.Q<4C'Z[5E(,G656//6!и!/!и!и/+1и1/+3и3/EX&/& >YEX/>YEX/>Y& 0и101!>54.#"#.546;.54>323#*"#7&';' 'L0H10C)! \J^o:ZzJ !K{Y8o`J >TkAZS&%P[DpV< !lc +//+01%#"&=!.5463!c 4o  =@@EX/ >YEX/>YEX/>Y 017>3+#"&'=]rdx * =1z+и,EX/ >YEX-/- >YEX/>Y'+-и01#"&54732>5#46;54>32.#"3#.9#.#-$/8"/#-$>C  82 G=D  73D z"B-8++01>3232676#".#"&>3232676#".#"&%!!+1)%%""-%% +<      p        <EX/ >YEX/ >YEX/>Y01>32!7!# q)> E d)%EX/ >YEX/>Y01 ȥbb" +$.8B +и/ и/ %и%/*EX / >YEX/ >YEX/ >YEX / >YEX!/! >YEX(/(>Y4/+и/и$и$/49и/>и>//?и?/0146'4632'6'.7&547#"'746'&54772'.7     0         1    9 )f%EX/ >YEX / >Y012#".54>">54&#".2#"&#"3263232654#"#"&=463232654&#"#"&#"3262654&#"#"&#";2/.#"#"&#"&#"#"&#";27'32654&#"#"&#"32632#"&#"3263232654&#"#"=7632326732632#"&#"3263232654#"#"&=326=463R^33^RR^33^[#&-.&$" #     k    F       "   :   :      3^RR^33^RR^3!&"%%"&!' p     p         b   " 7   ^"   $[ q    a) v z +01&456;#  zO/+0132>76#".546 (+  ! & %$ < W////EX/>YEX/>YEX / >YEX/>Y01632#"&'7632#"&'v p  w    =tEX/>Y 017623267#"&54>S  #%  " ''("9l& ///EX/>Y01#"'%&547%463!#f  mp  x  9e&// /EX/>Y01%>32.547463!#9   np    /&78/'/8 и /')и)/ .EX/ >YEX/>YEX / >YEX/>Y+301#"&54632.'.?.'63274'&#"32>"<7 %F9Y\\X(A /%_H# $R1VHO'6#09 Ev:lS2~~~6_*& ! 1-Uop>Y80NdB8w.//&//и/и&EX/ >YEX/>YEX/>YEX/>YEX/>Y!+01>32#"&'"#"&546332>54.#"c5 '>+')'$- -51#"0"6 3T@B`B)d6gV:L,@^"y#//#и/ иEX/ >YEX/>Y+ + и/0146;32+#4.+:2126@ V/@'\RO  4'THD2C%jh7*`<| 7+/EX/ >YEX / >Y0174632#"&&63#< >j5+EX/ >YEX/>Y 01#"&54732>5463j -%5:2&! %#:) #2!'G.//%// и /и/  и / %и/%"и"/%* //%/*///////01#"'.54676:3"#"'.54676:3A  k I!,0 &,,$!,0B@1 6n*Bj@vX4 8 b j R X ,V6P.H2J~ $Ff,V:Z r !P!r!"4"##$$$$%*%&:&F&R&^&j&v&'''''''((((&(2(>(J(V(b()H)T)`)l)x)*(*4*@*L*X*d*p+,",.,:,F,R,^,j,v,,,,,,,-----....D.//////0H00011\1t11122t23@3`3p44525l66678~9 ::;;< <=v>,>???@.@x@AFABBBC C2C^CCD^DE6E|EFG6GH>HIIIJFJK KKL|LLMlMNNN:OPPPQ&QdQQRSS~SST~` B    "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'    " & . 0 9 : ;% <= ? A B C H K L M O P R S T U V W X [ _ f g h i j k l m t3 u w x y z { }              )x . 3 7 9 A B C E M O f g h i j k l~    " & . 0 3 4 5 6 8 ; A B C D H: M O R S T U W m w x y z { } ~    )2356789;DRTUVWXdfghijkl"&.034568;ABCDMORSTUWmwxyz{}~, - __  !"&).03*56*78#9:;$<=s?ABCDEGHKLMNOPQRSTUVWXZ[_bdefghijklrmpqtuvwyz{|}#5#%,AN,,NN,)35678;<= = ==  )7=.?BCEfghijkpqvyz{==353Hg )fghijkpqvyz{)3578HPV   ,  " & . 0 3 5 6 8 ; @ A B C D M O R S T U W _ m w x y z { } !! ! !! !)!3!5!7!8!=!R!T!U!V!W!f!g!h!i!j!k!l!p!q!v!y!z!{!!!!!!!!!!!!!!!!!!!!!!!"" " """"""""&"."0"2"?"A"B"C"D"M"O"R"S"T"U"V"W"X"_"m"w"x"y"z"{"}"""""""""""""""""""""""""""""""""""""""""""""""""## # ## #)#3#5#6#8#=#?#M#O#V#f#g#h#i#j#k#l#p#q#v#y#z#{#####################################$ $$$$?$A$B$C$M$O$P$S$U$V$W$X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ $ $ $ % % p%%p%%%% %)%=a%?%A%B%C%E%M%O%P%R%S%T%U%V%W%X%_%d%f%g%h%i%j%k%l%p%q%v%y%z%{%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %p% % %p%p%%% & &&8&T&V&W&&&&'() ) ))) )))=)V)f)g)h)i)j)k)l)p)q)v)y)z){)))))))* * **#*"*)*.*0*?*A*B*C*D*M*O*R*S*T*U*W*_*m*w*x*y*z*{*}********************************* * * * ******** +f+ + a+++++"+&+.+0+2+3+5+6+8+?+A+B+C+D+M+O+P+R+S+T+U+W+_+m+w+x+y+z+{+}++++++++++++++++++++++++++++++++++++f+f+f+f++++++++f.. . . .. .)...3.5.6.7.8.=.A.V.f.g.h.i.j.k.l.p.q.v.w.x.y.z.{.}................/ / ///// /)/7/8/9/=)/?/A/B/C/M/O/V/_/b/f/g/h/i/j/k/lY/p/q/v/y/z/{////////////////////////////////////////00 0 00 0)030507080=0V0f0g0h0i0j0k0l0p0q0v0y0z0{0000000000000011"1.13151617181B1C1M1O1V1W1l1m1w1x1y1z1{1}1111111111111111122)2=2D2T2U2V2W2l222222222222233 3 33333333 3"3&3)3.303=3?3A3B3C3D3E3F3G3H3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3_3d3f3g3h3i3j3k3lu3m3p3q3v3w3x3y3z3{3}3333333333333335333333333333333333333333333333333333333333335 5 5555555 5"5&5)5.505=5?5A5B5C5D5E5G5H5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5_5d5f5g5h5i5j5k5l5m5p5q5v5w5x5y5z5{5}55555555555555555555555555555555555555555 5 55 5 55555555555 66 6666666 6"6&6)6.60696=6?6A6B6C6E6K6L6M6N6O6P6Q6S6T6U6V6W6X6_6d6f6g6h6i6j6k6l6m6p6q6v6w6x6y6z6{6}666666666666666D6666666666666666666666666666666667 7 7777"7&7.707?7A7B7C7D7M7N7O7R7S7T7U7W7_7l7m7w7x7y7z7{7}777777777777777777777777777777777 7 7 7 77777777 88 8888888 8"8&8)8.80828=8?8A8B8C8D8E8G8H8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8_8d8f8g8h8i8j8k8m8p8q8v8w8x8y8z8{8}888888888888888888888888888888888888888888888888888888899 999?9A9B9C9D9E9M9N9O9P9R9S9U9V9W9_9999999999999999999999999999999999999999999: 4:::HM;; ; &;&;#; .;f.;g.;h.;i.;j.;k.;p.;q.;v.;y.;z.;{.;.;.;.;.;;;&;;;&;&;< =)==g====="=&=.=0=3=5=6=8=A=B=C=D=E=H =M=O=R=S=T=U=W?? ?D?R?V?W??????????????@@ @ @@@=@R@U@X@@@@@@@@@AA AAAAABACAMAOARA_AAAAAAAAAAAAAAAAAAAABBBBCC C CCCRCUCVCCCCCCCCCDDD tD D DDDDtD;yD<D=D@DADBDCDEDF DKDMDNDODPDSDUD^?D_D`>DDDDDDDDDDDDDDDDDDDDDDDDDDDDkDDDDDDDDDHDEEEAEBECEHxEMEOEVEWEX EEEEEEEEEEEEEEEE EEEEEFFFRFUFWFFFFFFFFGHHHDII II#IAICIMINIOIUIWIIIIIIIIIIIIIIIIIIIIKK KKDKUKVKWKKKKKKKKKKKKKLL LLVLLLLLMM M M MMM=MDMRMTMUMVMWMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNN N N NNNN=N?NDNHNRNTNUNVNWNXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNOOOOOOOPP P PPPP=PAPBPCPEPMPNPOPPPVP_PPPPPPPPPPPPPPPPPPPPPPPPQQ QQQVQWQQQQQQQQQRR R RRR#RRARBRCRERMRORRRSR_RRRRRRRRRRRRRRRRRRRRRRRRRRRSS SSSSSSTT T TTTTTTT=T?TATBTCTETITKTMTNTOTPTQTRTSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUU U UUUUUU=U?UAUBUCUEUIUKUMUNUOUPUQURUSUVUXUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUVV VVVVV?VAVBVCVDVEVIVKVMVNVOVPVQVRVSVUVVVWVXVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWW W WWWWW=W?WAWBWCWEWIWKWMWNWOWPWQWRWSWVWXWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXX XXAXBXCXEXMXNXOXRXSXUXVXWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ&ZH^[[ [[[[[\^_3_5_6_8`d dd)d3d5d6d7d8d9dTdUdVdWdXddee e eHefegeheiejekepeqeveyeze{eeeeeeeeeff f ffff"f&f.f0f3f5f6f8f;f@fAfBfCfDfMfOfQfRfSfTfUfWf_fmfwfxfyfzf{f}ffffffffffffffffffffffffffffffffffffffffgg g gggg"g&g.g0g3g5g6g8g;g@gAgBgCgDgMgOgQgRgSgTgUgWg_gmgwgxgygzg{g}gggggggggggggggggggggggggggggggggggggggghh h hhhh"h&h.h0h3h5h6h8h;h@hAhBhChDhMhOhQhRhShThUhWh_hmhwhxhyhzh{h}hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhii i iiii"i&i.i0i3i5i6i8i;i@iAiBiCiDiMiOiQiRiSiTiUiWi_imiwixiyizi{i}iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijj j jjjj"j&j.j0j3j5j6j8j;j@jAjBjCjDjMjOjQjRjSjTjUjWj_jmjwjxjyjzj{j}jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjkk k kkkk"k&k.k0k3k5k6k8k;k@kAkBkCkDkMkOkQkRkSkTkUkWk_kmkwkxkykzk{k}kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkl ll?lAlBlClElMlOlPlSlUlVlWlXllllllllllllllllllllllllllllllll l l l l mm mmm"m&m.m0m2m?mAmBmCmDmMmOmRmSmTmUmVmWmXmmmwmxmymzm{m}mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmn nn?nAnBnCnMnOnPnSnUnVnWnXnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn n n n n o oo?oAoBoCoMoOoPoSoUoVoWoXoooooooooooooooooooooooooooooooo o o o o p pppp"p&p.p0p3p5p6p8p;p?p@pApBpCpDpMpOpPpQpRpSpTpUpVpWpXp_pmpwpxpypzp{p}ppppppppppppppppppppppppppppppppppp p p p pppppppppppp q qq qq"q&q.q0q3q5q6q8q;q?q@qAqBqCqDqMqOqPqQqRqSqTqUqVqWqXq_qmqwqxqyqzq{q}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq q q q qqqqqqqqqqqq s t/t 4t/t/t/t/t/uu uuuuuvv vv"v&v.v0v3v5v6v8v;v@vAvBvCvDvMvOvQvRvSvTvUvWv_vmvwvxvyvzv{v}vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvww w ww w)w.w3w5w6w7w8wAwVwfwgwhwiwjwkwlwpwqwvwwwxwywzw{w}wwwwwwwwwwwwwwwwxx x xx x)x.x3x5x6x7x8xAxVxfxgxhxixjxkxlxpxqxvxwxxxyxzx{x}xxxxxxxxxxxxxxxxyy y yyyy y"y&y)y.y0y3y5y6y7y8y;y@yAyByCyDyMyOyQyRySyTyUyVyWy_yfygyhyiyjykylymypyqyvywyxyyyzy{y}yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzz z zzzz z"z&z)z.z0z3z5z6z7z8z;z@zAzBzCzDzMzOzQzRzSzTzUzVzWz_zfzgzhzizjzkzlzmzpzqzvzwzxzyzzz{z}zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz{{ { {{{{ {"{&{){.{0{3{5{6{7{8{;{@{A{B{C{D{M{O{Q{R{S{T{U{V{W{_{f{g{h{i{j{k{l{m{p{q{v{w{x{y{z{{{}{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{|}} } }}}} })}.}3}5}6}7}8}?}A}B}C}E}F}I}N}O}P}Q}R}S}U}V}W}X}f}g}h}i}j}k}l}p}q}v}w}x}y}z}{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}"&.03568;@ABCDMOQRSTUW_mwxyz{}"&.03568;@ABCDMOQRSTUW_mwxyz{}  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}  DRVW DRVW DRVW DVW DRVWDRVW DRVWX ABCMOR C CRUV CRUVW CRUVW - F-----.FVW.....1 FVW11111 VW ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DERUVW   VW VW ?ABCEIKMNOPQRSVX ?ABCEIKMNOPQRSVWX  ).35678?ABCEMOPSUVWXfghijklpqvwxyz{}   ?DIVWX"&).03568;@ABCDMOQRSTUVW_lmwxyz{}VW  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{} "&.03568;?@ABCDEMNOPQRSTUVW_mwxyz{} ?ABCEIKMNORSUVW )2356789;DRTUVWXdfghijklpqvyz{ )2356789;DRTUVWXdfghijklpqvyz{   "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'   "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'"&.034568;ABCDHwMORSTUWmwxyz{}~   "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'   "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'"&.034568;ABCDHoMORSTUWmwxyz{}~"&.034568;ABCDMORSTUWmwxyz{}~358 )35678DRUVWXfghijkpqvyz{  t  c;y<@ABCDEF KMNOPSU^?`>kHVW t  c;y<@ABCDEF KMNOPSU^?`>kH       "&).035 6 8; ?ABCDEKLMNOPQRSTUVWX[efghijkl#mpqt!uvwxyz{}0,'! *7*Lv | u   "/Qk  ,A m >{ ,  * T)  }   8 8 D 43 "g Copyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone KaffeesatzThinYN: YanoneKaffeesatz-Thin: 2010Yanone Kaffeesatz ThinVersion 1.002YanoneKaffeesatz-ThinYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone Kaffeesatz ThinCopyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone Kaffeesatz ThinRegularYN: YanoneKaffeesatz-Thin: 2010Yanone Kaffeesatz ThinVersion 1.002YanoneKaffeesatz-ThinYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone KaffeesatzThin   "#$%&'()*+,-./0123456789:;<=>?@BCDEFGHIJKLMNOPQRSTUVWXYZ[\]_bcdefghjikmlnoqprsutvwxzy{}|~     !A^`a.nullperiodcenteredlongsEuroffffifflt_zg.ss01f.ss02ampersand.ss03ampersand.ss04germandbls.ss05z.ss05t_z.ss05uni2074Deltauni00A0macronuni0237` :latncpspkernP6Pk~ ~\&*~  4 ~ H X.X8L$\nNN`" X!!"##"#0#6#<#B#L#$d%.%&'(V()*|+, ---- -2-.z.01 2228363455606b6667*7t778808Z8l8~8889b9:;$; >??@x@ABCdD"DEFtGHlIKKL`LnLLMMMMNNNU 0; KOP[efghijkl#mpqt!uvwxyz{}0,'H 0:;%<=KOP[fghijklmt3uwxyz{} Ofghijkl~)0;Omwxyz{}~;fghijkl)0;Omwxyz{}~k, _ !0:;$<=sKOPZ[befghijklrmpqtuvwyz{|}#5#%,ANN;<= = ==)=.fghijkpqvyz{==fghijkpqvyz{2 ,0;Omwxyz{}# =fghijklpqvyz{< 0Omwxyz{}2 =Ofghijklpqvyz{& OP > p=aOPfghijklpqvyz{ pp =fghijklpqvyz{5 #0Omwxyz{} ;f a0OPmwxyz{}ff  =fghijklpqvwxyz{}9 =)ObfghijklYpqvyz{ =fghijklpqvyz{Olmwxyz{} =lS 0=KOPfghijklumpqvwxyz{}5O 0=KOPfghijklmpqvwxyz{} G 0=KOPfghijklmpqvwxyz{}D7 0Olmwxyz{} M 0=KOPfghijkmpqvwxyz{},OPI 0:;%<=KOP[fghijklmt3uwxyz{} &#f.g.h.i.j.k.p.q.v.y.z.{.....&& )g0O   =O /  t;y<=KOP^?`>kHO #O $ =% = =OP #O- =IKOP- =IKOP1IKOP, =IKOP%O&fghijkpqvyz{2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}& OP < 0Omwxyz{}& OP & OP = 0;OPmwxyz{} =  0;OPmwxyz{} ///1 0;Omwxyz{}  =fghijklpqvwxyz{}  =fghijklpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}? =IOPfghijklpqvwxyz{}10;Omwxyz{}10;Omwxyz{}M 0=KOPfghijkmpqvwxyz{}       O  ---...111% =I% =I% =I% =I% =I$ =, =IKOP- =IKOP@ OPfghijklpqvwxyz{} %  I30;=Olmwxyz{}M 0=KOPfghijkmpqvwxyz{}= 0;OPmwxyz{}'IKO/  t;y<=KOP^?`>kH;fghijklpqvyz{;fghijklpqvyz{U 0; KOP[efghijkl#mpqt!uvwxyz{}0,'U 0; KOP[efghijkl#mpqt!uvwxyz{}0,'(0;Omwxyz{}~U 0; KOP[efghijkl#mpqt!uvwxyz{}0,'U 0; KOP[efghijkl#mpqt!uvwxyz{}0,'(0;Omwxyz{}~)0;Omwxyz{}~fghijkpqvyz{;fghijkl,  c;y<KOP^?`>kH0  c;y<=KOP^?`>kHU 0; KOP[efghijkl#mpqt!uvwxyz{}0,'.  "&).35 6 8:<?ABCDELMNQRSTUVWX )x.0<wxyz{}.:i.DHv $V~j&    $ : 4 @ H "Dj,VTL~42X(zJhvR\379ABCEM <"&.34568ABCDH:MRSTUW, -_ "&).3*56*78#9?ABCDEGHLMNQRSTUVWX_dx,,N, )35678  )7<?BCEOl3Hg )l )3578HPV  )3578<V 4HM & .<l.& <1"&.3568ABCDEH MRSTUWmwxyz{}RUW <DUVWH^ < < Hl "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_?ABCEMSUVWX  "&.2?ABCDMRSTUVWX?ABCMSUVWX  ?ABCMSUVWX  "&.3568?@ABCDMQRSTUVWX_  "&.3568?@ABCDMQRSTUVWX_   <  4<4/// <"&.3568@ABCDMQRSTUW_  ).035678<AV  ).035678<AV!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_  ).035678<?ABCEFNQRSUVWX"&.3568@ABCDMQRSTUW_"&.3568@ABCDMQRSTUW_$ "&).2?ABCDEGHLMNQRSTUVWXl < <DRVW <DRVW <DRVW <DVW <DRVWDRVW <DRVWX <ABCMR <C <CRUV <CRUVW <CRUVW < <FI---FIVW... <FIVW111 <VW <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DERUVW < < <VW <VW?ABCEMNQRSVX <?ABCEMNQRSVWX ).035678?ABCEMSUVWX  <?DVWX"&).3568@ABCDMQRSTUVW_VW$ "&).2?ABCDEGHLMNQRSTUVWXl"&.3568?@ABCDEMNQRSTUVW_ <?ABCEMNRSUVW )2356789<DRTUVWXd )2356789<DRTUVWXd  "&).35 6 8:<?ABCDELMNQRSTUVWX  "&).35 6 8:<?ABCDELMNQRSTUVWX"&.34568ABCDHwMRSTUW"&.34568ABCDMRSTUW358 )35678<DRUVWXl < t@ABCDEF I MNSUVW t@ABCDEF I MNSU < < < <||40 pp fff  t  xD   o  9f{}0678+.3$5=*?I3KX>Z\L^^O``Pd}Qk      00:=FFKKOOZ[eqs{&}}/0JQX]abdg  &)+./135:<<?ACF!HI%KN'PX+__4dd5fq6w{B}}GHIU[bjrsuvx)23+*+11 23 !"#$%&'/0 """"&& '**,-.,-.+/0*().&& /%.!#*+,*''(')  "$////-!!!!!!!++++' + )%"-$ @ latn  aalt>dligFhistLligaRss01Xss02^ss03dss04jss05pss06v   (08@HPX`hHFPb`^\Z^|Y|Xp GJ\PVPP&YH&QRDEXDEQXslixmpp-1.2.2/docs/_static/fonts/YanoneKaffeesatz-Regular.ttf0000644000175000001440000037306412473141653025301 0ustar mathieuiusers00000000000000GPOSAVn\GSUB@Q'TOS/2T#``cmap C cvt sfpgmY7 sgaspg nLglyf~headc6hheaYqT$hmtx|ukern42zloca maxpx nameggY0Ppost1kprep}k.8u!_< |%`"+2 89^n svG@ KYN @ 89  " 4,$A$"&%%O *",%""'/x+S,@,8,,+2*=+++h+u+fu+ lyJ  y b/9 i |-.}..3.~-p.-Y#,XdNCy,S##5 2gS@,@,@,@,&+G$++++l'bbbbbbE9iiii*~-ppppp",,,,dd.OWfYlN&X&%"""E"E"B") $, "   ^ h r% &!&\=},,((,e,b,I'+0) fSn\#!^(",,/ B(R##"/  ( @,,,,.h+*n&   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYjkmov{[`^a]l}\eZ_dfizbhpgqnsturxyw~cHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcdHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcd,K PXYD _^-, EiD`-,*!-, F%FRX#Y Id F had%F hadRX#eY/ SXi TX!@Yi TX!@eYY:-, F%FRX#Y F jad%F jadRX#Y/-,K &PXQXD@DY!! EPXD!YY-, EiD` E}iD`-,*-,K &SX@Y &SX#!#Y &SX#!#Y &SX#!#Y &SX#!@#Y &SX%EPX#!#!%E#!#!Y!YD- ,KSXED!!Y-+++D8,+?4(+N@2$+N@.$++ E}iDK`RXYGMQX B    k+// /EX/ >YEX / >Y ++ и ии!и%и'и,и.01%#7##7#46;7#46;7>337>33+3#'37#?!&_!&7 *@ 3!&_!&: .C __C SC S&j&? O? O&j&@j4k9d5+9/EX/ >YEX/>YEX5/5>Y&+ и/01.54>732654&/.54675463.#"#:54'/.pB=&$9. 5#-4y 0$$&  ,)- r76AY y " $y>'4*W ,#3EW,<+4$+++KиK/<QиQ/EX/ >YEXL/L >YEX///>YEXF/F>Y/9014632#".732>54.#"4632#".732654.#"&'.5,JD!2!GD!3#N   JD!2!GD!3#M$   "9H%X _X*F5aZ(I9&0 /$%/ /_X*F5aZ(I7&0 4G%/ 0  =az?M[\//и/-8и8/\CиC/TEX/ >YEX/ >YEX!/! >YEX8/8>YEX>/>>Y '>Y012.#">54&'>32+#"&'#".54>7.546.'326CS 9"!"8F$# !  %1  + 'H47H*)#+X!E # %$1# Tbf./O`  $2#R*&  "#2A#80'32Z  J+1+A>/A=#" +/EX/>Y01#".54>32%+49-.:3,"CWuT <~‡}w9Qk=#" +/EX/>Y014.'>32#"&'>"-3:.-94,$CJmV7uB Sr )2/&632&67/4>&67'6.'     S s X  f  &,     "k?+ и//+и 017#5#46;54633#!& {!& { &!{ &!&k+ /+0174632#"&'>7&+7  7<6+  %= +01746;#%  &!&!%m $+EX / >Y0174632#"&% 3# ! E //013#>3d"" XP//и/EX / >Y+ 014632#".732654&#"UaaQ+F44D)W #3-%49*(Q|R*(MrD>V5ygh~X#2 +EX/>Y и01.54>323:767#!.54>3E2A>  J   *3 &  *&  X.V )+EX/>Y,+,9и/и/'901.54>32326767#!.54>7>54&#";0A$+9"/GT$*  !PE/#*'R!  #2!4\Z`9.&  7^VP* %*yX67+ +-#++и/01#"&546732654.#'>54.#".546320$"BG,9@E)F]P(5 '-$6%>_?  aK (4P*  " "!+5-%^%,n&+и&"/EX/>Y+и/и/и&и"'01%:767+#5#.54>7632<  3! "+..T -*# h ( ;KVN@| FQR""{X0O'+и/"+0 +,+ и/ и/01#"&'&'>32#".546732>54.#"y  @!(-E;'9W<, "?):%)9#3X (LAClM* "8I&+7 '"0k1/./1и/%и. EX/ >YEX/>Y#+)01">32#".54>32."32>54& !8(B0@Q`^:C# #=S0'/GB/02'-q>bE$$Zh|,SuHp\' Jug,<#BF,X/+ и /01"&5>7#"&5.O>, .% /YEX/>Y7+791?19012#".5467.'.54>.'32654&">54."A1' ,A,2F-B0" .>] +<81.6A);' &7!%?0!4*"  C-'F34F(Ab 7+A+[  G26@<6),h+/2  ?*" a.Q//#/и// и /# ,+)+ +012>7#".54632#"&546726754.#"0A*@06'`^6B% w%5"l 9$3<0%D\8##3K1z IxW "&8U9TUBF% B+ иEX/>Y +014632#"&4632#"&%  # !# !" 7+ и /и/// +014632#"&4632#"&'>7&# 7  # !<6+  " ++01463!#463!#" M  M S&!&!&!&! ;iYEX / >Y 10174632#"&2"'&'.5467>54.#".54>J F?L#&   #!) $,3# !EF-D7/  % $1-/  /Bcr +)+YH+l;+HCиC/HEиE/Hdиd/EX/>YEX0/0>YEX6/6>Y$+VK+@g+6o014.#"3267#".54>32#"&'#".5>32474656&#".567>3232>.#"3267a3S>@bB"YEX/>YEX/>Y+ 01%##"&'632#*/3'.'1'   Ɗ"  S 43..25+g$1o2/+/2и/+$%EX/ >YEX/>Y1+#%0146;2+32654.+32>54.++ c<0")$4E'~S1 ) 0-!". "=0);' 8-?Z:7<&'<+'-B'C#+EX/ >YEX/>Y 012.#"3267#.54>$ "-$%1D8$<-%9F   @ycLa7  +FmS,lY// и /EX/ >YEX / >Y 012+4634.+32>1J12I.}  &.-%Hy[nZ& EZ4C},,U+EX / >YEX/>Y+ 01%+47>;+3+)I,,K+ EX/ >YEX/>Y+ 01347>;+3+#,  .`)k*//и/*%и%/EX/ >YEX / >Y+  012.#"32675#54;#".54>38 )&7#'IC42H-7O CqTYj94LiuR,m//и/ ииEX / >YEX/ >YEX/>YEX/>Y+01#"#4>334>3"#!!!!T T+} /+EX/ >YEX / >Y014>3"#+""T5+EX/ >YEX / >Y014>3#".546732>5" 5)) + %@.   -*|!{+EX/ >YEX/ >YEX / >YEX/>YEX/>YEX!/!>Y014>3>32#"'.'.'"#*! - YEX/>Y0173+4>3} !L<  +<{=//=и/и/0EX/ >YEX/ >YEX/>YEXY0146;4>7>;"#7>7+'.'"#+ ;_  XB"5#4! $"!$  T$\WD GA3 3BH55-e(+*+/!/+и/ и/!&!*и*/EX/ >YEX&/& >YEX/>YEX)/)>Y01.'"#4;5.=4>3+ !>p !=GSSX\OGRQV[PT'](//(и/ EX/ >YEX/>Y#014>32#".732>54.#"0H01D*.H10E+[ #% #% bdS$"QegV% SXFb?DjMKgAEo+We//и/ EX/ >YEX/>Y +014;2+"#4&+326+c_Y3C%#!#,2,*+]i>S35AEw$8s9///9и// и/%EX/ >YEX / >Y+ *4014>323267#"&'.732>54.#"0H01D*-! 4>+<&[ #% #% bdS$"QeRzV4 (!   =D%SRFb?DjMKgAEo+^&/0/'/0и/и/' ' и / и/+ EX/ >YEX/>YEX/>YEX&/&>Y-+*0147>;2#"&'.'"+"#4&+326+c_Y0'     #!"-2,*+]iQ]2F^; ?`E- 9BCO/0/&/и/0-и-/ -и/EX/ >YEX/>Y ##9) 9012.#"#"&546732654&/.546H; 4#-4l)E3D? 3$6,!e"W  % ,{)))&D4' 8.":&u*C#?QmA+EX / >YEX/>Y и01"##543!#!Tpp;<+`//и/EX/ >YEX/ >YEX/>Y01732674>3#".54>3% 1 "[<YEX/ >YEX/>YEX/>Y01&6;>7>;#"&'.'&BS  J G  }C00Bk A Wck3x?qEX/ >YEX/ >YEX/>YEX$/$>YEX3/3>YEX6/6>Y-01&6;3>7>;3>7>;#"&/.'#"&'.'&CH  = :F  @ E  3 7  }Cn0>>9"Vcj3v #GEX/ >YEX#/# >YEX/>YEX/>Y01+'.'+&>23>?>23 Y6 6 F}($(&" p(OO)} jDq+QQ,dh@+EX/ >YEX/ >YEX/>Y01&>23>?>23"#'#(  #"!?=2 2=?zepMEX/ >YEX/>Y 9  90143!3267#!.547#",+   @  /y9x5?+EX / >YEX/ >Y 01+3+4>3x   5  @ //01#&>3>""   86C+EX/>YEX/ >Y 014>;#4>;#    g  k +01463!#  M m&!&!;EX / >Y01".'>32(0+ $,* ;#!  '( =(=>/ />и/и/  и/ )и1 9и9/EX/ >YEX/>Y,+#601467>32#".54>3254&#"..#"3267>4 R9A;F14(%0) &H   6D5+WQH  =34@$ O# '!"  ',./e#r$//$и/EX / >YEX/ >YEX/>Y !014>3>32#"'732>54&#"/ 4#.%9DO&Q ""'.K7lC71\LP>&%C +EX#/# >YEX/>Y#01.#"3267#".54>32& &# !* >2$7&!4@#0  )OC5D( 6bOZk9 R&a'//и'и/EX$/$ >YEX/>Y$ 01.#"32674>3#".54>32   G/!;-*8 -+N=8F' l5 2`QJjCO#34/1/4и/ и/ )и)/1.и./EX/ >YEX/>Y) +$012#3267#".54>":>7>5.:92 ":&2#;*1@$%# !F(4  3\I^q<G'A3 6- V(+"и'EX/ >YEX/ >YEX&/&>YEX / >Y !и"014>;54>32.#"3++ 01;./$$ZI A <:G&   ' C a;:H]^/Y/ и/^*и*/O3и3/EX/ >YEX/ >YEX%/%>YTIܹܸ;C0123#"'#"'#".5467.7>7.54>"32654&"&'32>54.  xKW  +0#/I22?# , !"#,=" -($%< !& ,6%P_   ( ;- &%=/O02F+<7B*BB7/Q0  -V#}$//$и/EX / >YEX/ >YEX/>YEX#/#>Y 014>3>32"#4.#""#- < )" (!!2c y.&/%EX / >YEX/>Y01<&4+EX/ >YEX/>Y 01.c4=y+.и:./EX / >YEX/ >YEX / >YEX4/4>Y 7.;014>3>32"#>54&'."#4#">. D   $9$" 1"%+4D3'TE  <1)|?. /+EX / >YEX/>Y017"#4>3! . - +'+ +EX/ >YEX/ >YEX / >YEX / >YEX-/->Y$и$/01.#""#>32>32"#4&#""##!!!L* ,B%-!!!j$4"r$  m-Xh//и/ EX/ >YEX/>YEX/>Y01>32"#4&#""#-&*+O>"&)  =I`0' kY#]$//$и/EX/ >YEX / >Y0174632#".732>54.#"WO';&TP':(X   {;cL{7gR>J( ,J;YEX/>YEX / >Y$01632#"&'#"&532>54.#".?S%<+,5(  Q   "(/Q=e{B  0[N-8! :W'v(//(и/иEX/ >YEX/>YEX / >Y $01#"'5#".54>32&#"3267W A0' 8K*#1R"  # ,1YJSpD64T=3>" -9+EX/ >YEX/>Y 01>32&#""#-F%& #" w@*+/!/и/+(и(/ (и/EX/ >YEX/>Y 9$ 9012&#"#"&546732654&/.546'1 +7#,] XM6D+9&+a"O K 7&CH  &S54B+p +и  /EX/ >YEX/ >YEX&/&>Yии&01#4>;54>33+3267#".5VD2 fV &    '7",W\//и/EX/ >YEX/ >YEX/>Y01%#".54>332674>3WO)-:" " )%!*>*G1' H6EX/ >YEX/ >YEX/>Y01&>3367>?>3#"&'%!  !s $  'Y&,+,,&X( QGEX/ >YEX(/( >YEX4/4>YEXE/E>Y01&>33>?>;3>?>3#"#"&/.'"#"&'.'&$   5'           *V#)' +35 ? ;BBo/[F+CDB<=BCt>FK$T/GEX/ >YEX/// >YEX/>YEX/>Y01"#'.'"#'&>23>?>23(%%  "y%"%  "  A%$! !$%1D!  6?T(<EX/ >YEX/ >YEX/>Y%01&4>34>?>3#"&54673267" 6  ,"{ !+-$ $'  :2$#2= 0?'  .>:$UEX/ >YEX/>Y 9  9013267+&'&547#".5431X*0 f.  h#y8/ "+ /EX/>Y01#4>3!" ,; I+и//EX / >YEX/>Y 01#"&54632#>3 ,## "v  S{Yq1m(+"/1/EX/ >YEX-/- >YEX/>YEX#/#>Y 01.#"3267#5.54>75463# !% #,0 %%($.&$   )QE:B!   \ 4WDId?!W 60+и0*EX/ >YEX#/# >YEX6/6>Y+ + $и(и+и/017#46;5'#46;&>23>?>233+3+"# bn 7_'#'  %"_Y Q b!%':%96, ,69z%5,%L  +и 014632#"&74632#"&#@%5GI6&+ +.@+;1+)C++#+01.#"3267#".54>324632#".732>54&#"&       !mb1I0gc1K2>"3 3'BC 5&'! -&.6 tri3S&'?-UK&>%G%EX/ >YEX/>Y0174>7672#&'.74>7672#&'.  )   !"   )!  ( $ $ )! &+3<  $)00)#  <4,&"'.7  > &**$< 6.("#@!?Hc+69+%@++%+и+/@1и1/6D ++F4+014632#".732>54&#"72#*'.'##547>34&+326#mb1I0gc1K2>"3 3'BC 5&*'    Uri3S&'?-UK&>S&#! 0-1,\FV9 //01"&'&'>32 ,( $+*9 *"   5G + +014632#"&5  # !2 ++01732#"&'&'67>763254&#"7R -#    <#'- "g %I%EX/ >YEX/>Y01%&'.54>767&'.546767&'.5467672.546767 !)   "!   (  !) $  (  &,4<  #)00)$ <3+&#(/5 ;$)#,*# 7/(#= ;iYEX / >Y  101#"&54632"&54>7>54&7>3267 F?L#&   # !*$-# !aEF-D7/  % $1./  & >e& a& [& Gz& ]d& xS"*"+"и$EX/ >YEX/>YEX / >YEX/>Y$+ +01%+5##"&'63!+3+'3OD%׆ Ij:;82BGWC+EX/>YEX>/>>Y3&+ +8+012.#"326732#"&'&'67>763254&#"7.54>$ "-$%17/ -#    5&%9F   CycLa7  (.#'- "YJ~eS,,&$>A,3&$a,4&$8,1z&$]@&(>&&(a&&(z&(]+&-R&.>g&.a&.^&.Jz&.]gGZ7&'7'>76'K YY KLYY Sbd TT cc$.9HI/:/I и /:$ /2и2/EX/ >YEX/ >YEX/>YEX)/)>Y5)D01".'&>7.54>32>7#"&'&#"4&'32>W   /G1&;   .H1'; 1&% **% "'  *zVdT$ !$OgV%Z1MEp<3N0xufg+&4a+&4^+z&4]gh&8a'@y3@+ (+#+EX/ >YEX/>YEX7/7>YEX9/9>Y 0014632#"&546732654.54>54&#"#"'&'67>5/XI7L0")"#(#PF<;) #*0:0#)#571$ &QJ)4"( "'0;%EN$%'?<>&"* +:C4 =&?>==&?a=&?3E&?=&?]<=&?P BScK1+)S+a+1и/!и!/S9EX/ >YEX / >YEX&/&>YEX,/,>Y=,P=TиT/01467>32>32#"&'3267#"&'#".54>3254&#"..#"3267"327>5.KB(0 ?$/*&>  %<&21=04($/*&&?   $4  #"G4=  ,   @84B&H#  +!$m ":,"6+2&Eh >+EXC/C >YEX/>YEX9/9>Y.!+3+C01.#"326732#"&'&'67>763254&#"7.54>32& &# !* 0( -#    /!!4@#0  )OC5D( 1#'- "\8^IZk9O&C>RO&CaO&CHO&C]Q&>*&a*&&]-Y&L3Y&M>FY&MaY&M=Y&M)Y&M]F"P;+и +++017463!#4632#"&4632#"&" M &!&!s? *9EF/+/F и /+ +-и-/ :=и=/EX/ >YEX/ >YEX%/%>Y5A01.54>7.54632>7#"&'4'32>'7.#"> ! WO#7   )='%9 ##  ~  M5{  8VFc@ + ?>8,J7#*K,W&S>O,W&Sa,W&SF,W&S]O?T&Wa?T&W]B. /+EX / >YEX/>Y017"#4>3! ;,@A//Aи/ии/,#и-EX/ >YEX/>Y%*+"2и2/"<и32547>;+3+32>54.#"7*O0E+/G1$8 $$ #% I(1!SkbT&Fc@EkMKiC Hq:,@PQ/N/Qи/N и/-NKиK/EX/ >YEX / >YEX#/#>YEX(/(>YF+(2<A0174632>32#3267#"&'#".732>54.#"%":>7>5.WN);E%:92 $<&2(@'L'<(X   ?$%# z %"G-5 , !67gR>J( ,J;YEX/>Y +014632.#"#"&'&'67>5.XU3B ,1( LQE   59 :// //01632#&'.'#"&'q $   &    7///01".'>32>767|$(#  '$7%%#  (*!TX  +01>32#"&! "2  ++014632#"&732654#"5/0-30.0:%#3/)811)7))>&%(EX/ >Y+01>3232>32#".#"#"&'&<  &  p*!      %= +017463!#% N &!&!= +017463!#  &!&!"?+ //01#"54>326 s;:. -"<+ //014632#"&'>7&$7 ;:. ."j+ //0174632#"&'>7&$7 6;9/ -"?##G$//$и/ и /и/ ////01#"54>32#"54>326 7 s;:. -#;:. -"<$#K$// и /$и/и/ ////014632#"&'>7&'4632#"&'>7&7 7 ;:. .#;:. ."$j#K$// и /$и/и/ ////0174632#"&'>7&'4632#"&'>7&7 7 6;9/ -#;9/ -)i + +014632#"&)&#)"(#) .*# ,)$m&'R,!5EYi{ʻjZ+br+F6+>P+"+,+EX/ >YEX/ >YEX/>YEXA/A>YEXe/e>Y9U+1AK9]иKoиUw01&'.54632#".732>54.#"4632#".732>54.#"4632#".732654.#"  "9H%XJD!2!GD!3#N   JD!2!GE!2#N   JC!2!GD!3"M#   =a{?[_X*F5aZ(I9&0 /$%/ /`W)F5aZ(H9&/ /$%/ 0"`W)F5aZ(H9&/ 4G%/ 0%%EX/ >YEX/>Y0174>7672#&'.  )   !"   )! %,3=   $*00(#  <3,% %%EX/ >YEX/>Y017&'.54>767&'.546767 !)   "!   (!  %,3<  #(00*$  =3,%7 %EX / >YEX/>Y01'#"&'>32 O    ^E8+и/83и3/8<иY +++.#3и7и<и@012.#"3+3+3267#".'#46;5<7#46;>(& %"  xy V /+":+X 6X ? )29^  1(% $*3  ,QC% !%@N+~1E38+-1++и/-*и*//1/8/EX/ >YEX/ >YEX/ >YEX=/= >YEX?/? >Y29и:01476;67>?>;"#74>7+/#"##5476;#,(  eR~ wo " PJNT( f"= +017463!#" M &!&!  7DE/6/Eи/и/и61(и13и3/8и69EX / >YEX)/) >YEX8/8 >YEX/>YEX5/5>Y#+8иии//и0и7и#?01+#4>;54>32>32.#"3++'3547.#" AE0!063=1+#$YH @zz  a E8B"    ' C aE<6%# v 56//6и/'.и'1и1/4!/EX / >YEX/ >YEX'/' >YEX/>YEX3/3>Y -и.01%"#4>34>;54>32.#"3++e! 0 4@ 8;8%F5 AE < a n-.//.и/и/&и)и)/,EX/ >YEX/ >YEX/>YEX+/+>Y +%и&014>;54>32+.#"3++ 0 /7&D A  P? @ <YEX!/! >YEXC/C >YEXO/O >YEX-/->YEX5/5>YEX=/=>Y+ и /'и(и/и0иI014>;54>32>32.#"3++#+"#4>3%47.#"3 0 073B"7<8%F5 Ay @!   y E8B"     )#> aaa<7$#N :;H-2++G+-$иG4и:и2<EX/ >YEX%/% >YEXG/G >YEX/>YEX1/1>YEX9/9>Y+ и /+и,и3и4иAиA/014>;54>32>32"#.#"3++#+747.#"3 0 0728/B!E4 Ay @  y E8B"    O& H aa7$#N]&RX#%8Y#45/$/ и /5и/$,EX/ >YEX/>YEX/>Y'101"&54732>=#".54>32.#"3267>C2)  3#!0!"8J)8->5! &#"C7]ESl@(:&u .N=4D)!& 'l+ и&EX/ >YEX/ >YEX%/%>Y +и 014>;54>32.#"3++ 0)*  ZH A t&0  &k a&?K++/?/.++'(+'$и$/;и;/017.5467.54675463.#"26732>7#UJ?,!UE%% 1! ?-4>+)% FH76,$ #7&$&9RG>I +!DU Q r&$0*##MA-(+ )O !lkx+g+]+)и>иDиD/gSиS/EX9/9>Yb+IX+9-014#"26767>>76754>3>76732767#".=#32>7#".54>323&1$) * &   *7% :70j* &&8#';(*C2-$ U]XPB3&  !YEX/>YEX/>Y+=2+#01#"&'&'67>54632.#"!#".546732654&'.'7 YV*,4%5a#  ID.1 zF)2"+EX/ >Y+01<>3!#".546732654&'.'7a#  ID.1 z78/0/8и/ии0/EX / >YEX/ >YEX/>Y-$+и01##4>;54>3!#"&546732654&'.'7!N< +a#  ID.1 l()//) и /и #EX/ >YEX / >Y +и !и #012+#46;4634.+3+2>1J12I.}(   &.O ?-%FvXq\&J&" EZ4&"C})M +и //EX/ >YEX / >Y0173+.?4>3}a  b "  "!%!<8 $" $ G +и  //EX/ >YEX / >Y017"#.?4>3+  ,!(  (  %! 3 %! =,!QA+EX / >YEX / >Y+01&5467>323#!.546;59  ,+#W  ]W  ,S7H/+EX/ >Y,!+5+и/ 014632>54&#".7>32#"&'&546732654&'.#.v &5 H7A86 $/@%*6 /#+3' * 4&:.1%6$    (!W(2 #+EX/ >Y+&01.54>323#!&5467>54&#"B -6$0 #39@4!( 1M  '$@:7 ) #:4/(o 'i + и!и!/ //EX/ >YEX / >Y+и!и!/013+#"&'5#&5467>7>327-< )+  ")-#I#! 3H ED<  +36,W+TU/O/Uи/$и$/O9EX / >YEX / >YEX(/( >YEX!/!>YEX/>YEXD/D>Y+4R+иD>01&5467>323#!.546;5#"&'>32.54>323#!&5467>54&#"9  ,+#W  ] O  Y -6$0 #39@4!( 1W      '$@:7 ) #:4/,9&'d,6&'aI: q+и////EX / >YEX / >YEX/>YEX/>Y01#"&'4632#"&'4632      b }v '_h //017#"/&5467%  `&l  +Um// /01'>32.'47  `%l  0|/ //017#"&'7'n  W)j;;!+и!7++/)+01#"&54>54&'.5467>54&54>$%  K?   )9  &'%\61* (-6Y(#  DC//1(! 6d--j9;2 + и2&++)-+01.547>54&5467.54654.'&54632!#%  K>  )9  &'%[62+ (-6Y($ EB//1(!  6d- ,  M!# ++и/01>3232676#".#"& ") %  )!         ,i&+&и/ и /EX/ >YEX#/#>Y +и/ и/01&632'76'.7&5467#"&'>  x xN     ?  _  S( ?//и/  ++014632#"&732654&#"S5/0-30.07(3.(820'9,QS*\+и&EX / >Y!+&+ и&01+.'#46;>32.#"3#> .*9$'<!# 6_/ EB&!E^; $@3&!#7J\y]/Q/и/и/]>и>/+и+/>ZEX/ >YEX%/%>Y %0012.#"#"&'.54732654&'.'.5467&'.54>>54&'.'14&#%   )8 14&#%  (8    '8 &+  (7'+| !   ! !&3++'-+EX/ >YEX/>YEX/>YEX*/*>YEX-/->Y01%#"&'5#".54>32.#"3267#"&'4632`-9,4I,&=R24" #   5XDMnF! 4jj6C% w!YEX/ >YEX/>Y+ +и ии 017#"'7#46;7#46;76323+3#%!T \( (&[ d( tj`&!s&!rl&!s&!(p6+>k?/7/?и/74+ <+ ии/и/4 и /014>32>32&'.'#"&73267'.'&732654&'&( 4&3<<.:G?4  %;>)ABQ) # 6)1"!2JHJE+" /OA*&#'  %##"V+ и/EX/ >Y++и 01#5#46;54633#463!#!& {!&  M !{ &!{ &!&!&!,=]"#//# и /EX/ >YEX/ >YEX/ >YEX"/" >YEX / >YEX / >YEX/>Y01%#"&'#"&'463232674632]R-"   % #   #,,z-// -и/и/%EX/ >YEX/ >YEX/>Y (012.'>32#".5464&'.#"32>6"I<  RS %C6-D-W'% 1-$Hi1 DԡYEX/>Y  01!+3#/ y|`(?//и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yи ии01#"&'##"'#.5463!#` l- Yo * *   // и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yи ии01#"&'##"&'#.5463!#` l - Yr r    <'2+EX/>Y+"014>32.#"#"&546732>51;6'#$1<5(#$4Y. +&+01>32#".54>3254&#".6.#"32674<2/;F')(# &1  "/7 1)$/ 9% &#!/!P"//"и/EX/ >Y +014632#".732654.#"#L?!0 GC1!E $ pc/Q>mg/Q:*6 AN*6 "7#56//6!и!/!и/!и/)/и//)2и2/EX$/$ >YEX/>YEX/>Y$ .и/013>54.#"#.546;.546323#  $%   3 Ze/@' F JXd8LiA9aL9m`P 8Mc;$NzU?fN6 "hf5+//+01%#"&=#&5463!f  *o e/=@EX/ >YEX/>YEX/>Y017>323+#"&'/ =X Fa"   <3z+и.EX/ >YEX/// >YEX/>Y )+/и01#"&546732>5#46;54>32.#"3#1<5(#${ j1;6'#$ <;E#   %!&!@3232676#".#"&>3232>76#".#".>.# >* >*  !)%  F*    &  q(        +EX/ >YEX / >Y 01>32!73# \d MM%EX/ >YEX/>Y01'7nnocd'4AL $+$и/ (и(/$0EX / >YEX-/->YB5++и/#и#/B=и=/5HиH/0146'&632'76'.7&5467#"&'7>.546776'.7   x xL    x y    ?  a     ?  ('d%EX/ >YEX / >Y012#".54>">54&#"&2#"&#"3263232654#"#"&=463232654&#"#"&#"3262654&#"#"&#";2/.#"#"&#"&#"#"&#";27'32654&#"#"&#"32632#"&#"3263232654#"#"=7632326732632#"&#"3263232654#"#"&=326=463R^33^RR^33^[#&-.&$+     k    F       "   :   :      3^RR^33^RR^3 &"$$"& ,,' p     p         b   " 8   ^"  $[ q    a) v Q  +01.546;#Q  7/+0132>762#".546* !+-"    (!',2/ / //01632632#"'#"&'| v     =EX/>Y 0176327#"&54>c     #/# $  %0.%,o&///EX/>Y01#"&/&5467%463!#   . #a%l  |&!&!,m&// /EX/>Y01'>32.'47463!#    ) 1`%l  &!&!,*=>/+/и/+> и /6и/+.и./EX/ >YEX / >YEX/>Y1901#".54632.'.?.'>3274&'.#"32><'( %C6-D-WI6" ^ :  !  E ''% 1-$w@nYEX/ >YEX/ >YEX/>YEX/>YEX / >Y *01>32#"&'#"&54>332>54.#" %<+.5$      !/Q>dzB  `y 1\M/9 +W$m%//%и/ иEX/ >YEX/>Y"++0146;32+"#4&#"22326+"#_Y3C%#!#,  *+}]i>S35@E* E+и//EX/ >YEX / >Y0174632#"&4>3#* ,#2# " :<5+EX/ >YEX/>Y 01#"&546732654>3 4(82"2&$!:,  -5G-W./#/. и /#) ///#/&/)///////01"#4.47>32"#.47>32Z  x  J+1+A>/+1+A>/ T.VrjV2 4 ^ :  n  bj$zt\zjd:\ lV4 8 ! !l!"x"#4#$H$p$$%P%%%&& &&$&'8'D'P'\'h't'''''''''((((((()))))))*+F+R+^+j+v++++++++++,<,,,--- -,-Z......//L///00N0f0~0001V12202@3P33445v56b7789X9d9:`:;>f>?(?|?@@@AJArAAB2BBCfCDDEfEFXFG6GGHnHI/?/A/B/C/M/O/V/_/b/f/g/h/i/j/k/ln/p/q/v/y/z/{////////////////////////////////////p/p/p//00 0 00 0)030507080=0V0f0g0h0i0j0k0l0p0q0v0y0z0{0000000000000011"1.13151617181B1C1M1O1V1W1l1m1w1x1y1z1{1}1111111111111111122)2=2D2T2U2V2W2l22222222222223 3 3 33333333 3"3&3)3.303=3?3A3B3C3D3E3F3G3H3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3_3d3f3g3h3i3j3k3l~3m3p3q3v3w3x3y3z3{3}33333333333333333333333333333333333333333 3 33 3 33333333333333 55 5555555 5"5&5)5.505=5?5A5B5C5D5E5G5H5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5_5d5f5g5h5i5j5k5l5m5p5q5v5w5x5y5z5{5}5555555555555555555555555555555555555555555555555555555566 6666666 6"6&6)6.60696=6?6A6B6C6E6K6L6M6N6O6P6Q6S6T6U6V6W6X6_6d6f6g6h6i6j6k6l6m6p6q6v6w6x6y6z6{6}666666666666666<6666666666666666666666666666666667 7 7777"7&7.707?7A7B7C7D7M7N7O7R7S7T7U7W7_7l7m7w7x7y7z7{7}777777777777777777777777777777777 7 7 7 77777777 88 8888888 8"8&8)8.80828=8?8A8B8C8D8E8G8H8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8_8d8f8g8h8i8j8k8m8p8q8v8w8x8y8z8{8}88888888888888888888888888888888888888888888888888888889 9 999?9A9B9C9D9E9M9N9O9P9R9S9U9V9W9_99999999999999999999999999999999 9 9 9 99999999 : ,:::H;; ; ";";; *;f*;g*;h*;i*;j*;k*;p*;q*;v*;y*;z*;{*;*;*;*;*;;;";;;";";< =1==x= ===="=&=.=0=3=5=6=8=A=B=C=D=E =HL=M=O=R=S=T=U=W?? ?D?R?V?W??????????????@@ @ @@@=@R@U@X@@@@@@@@@AA AAAAABACAMAOARAUAWA_AAAAAAAAAAAAAAAAAAAAAABB B B CC CCCCRCUCVCCCCCCCCCD$D{D pD !D DDDDpD;dD<3D=D@DADBDCDEDFDKDMDNDODPDSD^>D_D`?DDDDDDDDDDDDDDDDDDD{D{DD{D{DDD-DD^DDDDDDDDDVD{EEEAEBECEH]EMEOEVEWEXEEEEEEEEEEEEEEEEEEEEEFFFRFUFWFFFFFFFFGHHH9II IIIAICIMINIOIUIWIIIIIIIIIIIIIIIIIIIIKK KKDKUKVKWKKKKKKKKKKKKKLL LLVLLLLLMM M M MMM=MDMRMTMUMVMWMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNN N N NNNN=N?NDNHNRNTNUNVNWNXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNOOOOOOOPP P PPPP=PAPBPCPEPMPNPOPPPVP_PPPPPPPPPPPPPPPPPPPPPPPPQQ QQQVQWQQQQQQQQQRR R RRRRRARBRCRERMRORRRSR_RRRRRRRRRRRRRRRRRRRRRRRRRRRSS SSSSSSTT T TTTTTTT=T?TATBTCTETITKTMTNTOTPTQTRTSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUU U UUUUUU=U?UAUBUCUEUIUKUMUNUOUPUQURUSUVUXUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUVV VVVVV?VAVBVCVDVEVIVKVMVNVOVPVQVRVSVUVVVWVXVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWW W WWWWW=W?WAWBWCWEWIWKWMWNWOWPWQWRWSWVWXWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXX XXAXBXCXEXMXNXOXRXSXUXVXWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZZH][[ [[[[[\^_3_5_6_8`d d d)d3d5d6d7d8d9dTdUdVdWdXd d ee e eHlefegeheiejekepeqeveyeze{eeeeeeeeeff f ffff"f&f.f0f3f5f6f8f;f@fAfBfCfDfMfOfQfRfSfTfUfWf_fmfwfxfyfzf{f}ffffffffffffffffffffffffffffffffffffffffgg g gg gg"g&g.g0g3g5g6g8g;g@gAgBgCgDgMgOgQgRgSgTgUgWg_gmgwgxgygzg{g}gggggggggggggggggggggggggggggggggggggggghh h hhhh"h&h.h0h3h5h6h8h;h@hAhBhChDhMhOhQhRhShThUhWh_hmhwhxhyhzh{h}hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhii i iiii"i&i.i0i3i5i6i8i;i@iAiBiCiDiMiOiQiRiSiTiUiWi_imiwixiyizi{i}iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijj j jj jj"j&j.j0j3j5j6j8j;j@jAjBjCjDjMjOjQjRjSjTjUjWj_jmjwjxjyjzj{j}jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjkk k kkkk"k&k.k0k3k5k6k8k;k@kAkBkCkDkMkOkQkRkSkTkUkWk_kmkwkxkykzk{k}kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkl ll?lAlBlClElMlOlPlSlUlVlWlXllllllllllllllllllllllllllllllll l l l l mm mmm"m&m.m0m2m?mAmBmCmDmMmOmRmSmTmUmVmWmXmmmwmxmymzm{m}mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmn nn?nAnBnCnMnOnPnSnUnVnWnXnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn n n n n o oo?oAoBoCoMoOoPoSoUoVoWoXoooooooooooooooooooooooooooooooo o o o o p pppp"p&p.p0p3p5p6p8p;p?p@pApBpCpDpMpOpPpQpRpSpTpUpVpWpXp_pmpwpxpypzp{p}ppppppppppppppppppppppppppppppppppp p p p pppppppppppp q qq$qq"q&q.q0q3q5q6q8q;q?q@qAqBqCqDqMqOqPqQqRqSqTqUqVqWqXq_qmqwqxqyqzq{q}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq q q q qqqqqqqqqqqq s t4t <t4t4t4t4t4uu uuuuuvv$vv"v&v.v0v3v5v6v8v;v@vAvBvCvDvMvOvQvRvSvTvUvWv_vmvwvxvyvzv{v}vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvww w ww w)w.w3w5w6w7w8wAwVwfwgwhwiwjwkwlwpwqwvwwwxwywzw{w}wwwwwwwwwwwwwwwwxx x xx x)x.x3x5x6x7x8xAxVxfxgxhxixjxkxlxpxqxvxwxxxyxzx{x}xxxxxxxxxxxxxxxxyy y yyyy y"y&y)y.y0y3y5y6y7y8y;y@yAyByCyDyMyOyQyRySyTyUyVyWy_yfygyhyiyjykylymypyqyvywyxyyyzy{y}yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzz z zzzz z"z&z)z.z0z3z5z6z7z8z;z@zAzBzCzDzMzOzQzRzSzTzUzVzWz_zfzgzhzizjzkzlzmzpzqzvzwzxzyzzz{z}zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz{{ { {{{{ {"{&{){.{0{3{5{6{7{8{;{@{A{B{C{D{M{O{Q{R{S{T{U{V{W{_{f{g{h{i{j{k{l{m{p{q{v{w{x{y{z{{{}{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{|}} } }}}} })}.}3}5}6}7}8}?}A}B}C}E}F}I}N}O}P}Q}R}S}U}V}W}X}f}g}h}i}j}k}l}p}q}v}w}x}y}z}{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}~~~~~~"&.03568;@ABCDMOQRSTUW_mwxyz{} "&.03568;@ABCDMOQRSTUW_mwxyz{}  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}  DRVW DRVW DRVW DVW DRVWDRVW DRVWX ABCMORUW  C  CRUV  CRUVW  CRUVW % F%%%%%*F VW*****9 FVW99999 VW ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?D ER UVW    VW VW ?ABCEIKMNOPQRSVX ?ABCEIKMNOPQRSVWX  ).35678?ABCEMOPSUVWXfghijklpqvwxyz{}   ?DIVWX   "&).03568;@ABCDMOQRSTUVW_lmwxyz{}  VW  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}"&.03568;?@ABCDEMNOPQRSTUVW_mwxyz{} ?ABCEIKMNORSUVW $)2356789;DRTUVWXdfghijklnopqvyz{ $)2356789;DRTUVWXdfghijklnopqvyz{   "&)|.03 568;%?ABCDEKLMNOPQRSTUVWX[efghijkl<mpqt)uvwxyz{}~4)6   "&)|.03 568;%?ABCDEKLMNOPQRSTUVWX[efghijkl<mpqt)uvwxyz{}~4)6"&.034568;ABCDHTMORSTUWmwxyz{}~   "&)|.03 568;%?ABCDEKLMNOPQRSTUVWX[efghijkl<mpqt)uvwxyz{}~4)6   "&)|.03 568;%?ABCDEKLMNOPQRSTUVWX[efghijkl<mpqt)uvwxyz{}~4)6"&.034568;ABCDHQMORSTUWmwxyz{}~"&.034568;ABCDMORSTUWmwxyz{}~358 )35678DRUVWXfghijkpqvyz{ ${ p  R;d<3@ABCDEFKMNOPS^>`?{{{{-^V{VW${ p  R;d<3@ABCDEFKMNOPS^>`?{{{{-^V{      ~ "&)|.03 568;%?ABCDEKLMNOPQRSTUVWX[efghijkl<mpqt)uvwxyz{}~4)6!" 3@*X  u   ";]w  2P  D 2  0  TP     8 8 D 4Z " Copyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone KaffeesatzRegularYN: YanoneKaffeesatz-Regular: 2010Yanone Kaffeesatz RegularVersion 1.002YanoneKaffeesatz-RegularYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone Kaffeesatz RegularCopyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone Kaffeesatz RegularRegularYN: YanoneKaffeesatz-Regular: 2010Yanone Kaffeesatz RegularVersion 1.002YanoneKaffeesatz-RegularYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone KaffeesatzRegularG  "#$%&'()*+,-./0123456789:;<=>?@BCDEFGHIJKLMNOPQRSTUVWXYZ[\]_bcdefghjikmlnoqprsutvwxzy{}|~     !A^`a.nullperiodcenteredlongsEuroffffifflt_zg.ss01f.ss02ampersand.ss03ampersand.ss04germandbls.ss05z.ss05t_z.ss05uni2074Deltauni00A0macronuni0237` :latncpspkernPPk ` .6  * @ T  d:(d&D"X 0pb0bt6 l!"!"#0#6#D#J#P#V#`#$x%B& &'(j))*+*, ---"-4-F. ./0"142F2L3J3X3f4,46(6.6`6667(7Z7788>8h8888899:0:;\;<<<<<=>D>J?L?@@BBCDZDEfFHHJKpLLLMMMxN*N8NFOOO$V 0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6H 0:;<5KOP[fghijklmt:u wxyz{}  Ofghijkl)0;Omwxyz{}~;fghijkln)0;Omwxyz{}~k X{!0:;< =sKOPZ[befghijkldmpqtuvwyz{|}- 4 (RR;<= = ==)=]fghijkpqvyz{== fghijkpqvyz{2 0;Omwxyz{}# =fghijklpqvyz{< 0Omwxyz{}2 =Ofghijklpqvyz{& OP > ={OPfghijklpqvyz{   =fghijklpqvyz{5 0Omwxyz{};x {0OPmwxyz{}xx  =fghijklpqvwxyz{}9 p=>Obfghijklnpqvyz{pp =fghijklpqvyz{Olmwxyz{} =lS 0=KOPfghijkl~mpqvwxyz{} O 0=KOPfghijklmpqvwxyz{}G 0=KOPfghijklmpqvwxyz{}<7 0Olmwxyz{} M 0=KOPfghijkmpqvwxyz{}, OP I 0:;<5KOP[fghijklmt:u wxyz{}  "f*g*h*i*j*k*p*q*v*y*z*{*****"" 1x 0O   =O /${ ! p;d<3=KOP^>`?{{-^VOO $ =% = =OP O- =IKOP- =IKOP1IKOP, =IKOP%O fghijkpqvyz{2 0;Omwxyz{}2  0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2  0;Omwxyz{}2 0;Omwxyz{}& OP < 0Omwxyz{}& OP & OP = 0;OPmwxyz{} = $0;OPmwxyz{}   4 44 1$0;Omwxyz{}  =fghijklpqvwxyz{}  =fghijklpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}? =IOPfghijklpqvwxyz{}10;Omwxyz{}1 0;Omwxyz{}M 0=KOPfghijkmpqvwxyz{}      O    %%%***999% =I% =I% =I% =I% =I$ = , =IKOP- =IKOP@ OPfghijklpqvwxyz{} % I3  0;=Olmwxyz{} M 0=KOPfghijkmpqvwxyz{}=0;OPmwxyz{}'IKO/${ ! p;d<3=KOP^>`?{{-^V!;fghijklnopqvyz{!;fghijklnopqvyz{V 0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6V 0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6(0;Omwxyz{}~V 0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6V 0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6(0;Omwxyz{}~)0;Omwxyz{}~fghijkpqvyz{ ;fghijkln,${  R;d<3KOP^>`?{{-^V0${  R;d<3=KOP^>`?{{-^VV ~0;%KOP[efghijkl<mpqt)uvwxyz{}~4)6/   "&)|:<?ABCDELMNQRSTU/flR`j * 0>DZ|hT@ . p d n J  & *",Rx.p8V|T.Lj,JZ&<J`",v .3 568VWX! ).0379<ABCEMwxyz{} <"&.34568ABCDH.MRSTUW, $X "&).35 6789?ABCDEGHLMNQRSTUVWX_dx R )35678  )7<?BCEOl3H@ )l )3578H2V  )3578<V ,H " *<l*" <1"&.3568ABCDE HLMRSTUWmwxyz{}RUW <DUVWH] <  <Hll "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_?ABCEMSUVWX  "&.2?ABCDMRSTUVWX?ABCMSUVWX  ?ABCMSUVWX  "&.3568?@ABCDMQRSTUVWX_  "&.3568?@ABCDMQRSTUVWX_   < <<<444 < "&.3568@ABCDMQRSTUW_  ).035678<AV  ).035678<AV!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_  ).035678<?ABCEFNQRSUVWX"&.3568@ABCDMQRSTUW_"&.3568@ABCDMQRSTUW_$ "&).2?ABCDEGHLMNQRSTUVWXl < <DRVW <DRVW <DRVW <DVW <DRVWDRVW <DRVWX <ABCMRUW <C <CRUV <CRUVW <CRUVW < < FI%%%F I VW*** < FIVW999 <VW <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?D ER UVW < < <VW <VW?ABCEMNQRSVX <?ABCEMNQRSVWX ).035678?ABCEMSUVWX  <?DVWX "&).3568@ABCDMQRSTUVW_  VW$ "&).2?ABCDEGHLMNQRSTUVWXl"&.3568?@ABCDEMNQRSTUVW_ <?ABCEMNRSUVW !#$%')*+-/12356789<DRTUVWXd !#$%')*+-/12356789<DRTUVWXd  "&)|.3 568:<?ABCDELMNQRSTUVWX  "&)|.3 568:<?ABCDELMNQRSTUVWX"&.34568ABCDHTMRSTUW"&.34568ABCDMRSTUW358 )35678<DRUVWXl < p@ABCDEFIMNS{{{VW p@ABCDEFIMNS{{{ < < < <$41 xxxpp   {{{p]9 | | |Q z 9f{}0678+.3$5=*?I3KX>Z\L^^O``PdQ    00:=FFKKOOZ[eqs{'}0MT[`degj  &)+./135:<<?ACF!HI%KN'PX+__4dd5fq6w{B}}GHIU[bjrsuvx)23+*+11 23 !"#$%&'/0 """"&& '**,-.,-.+/0*().&&////////// 0%.!#*+,*''(')  "$/////0000-!!!!!!!++++' + )%"-$ @ latn  aalt>dligFhistLligaRss01Xss02^ss03dss04jss05pss06v   (08@HPX`hHFPb`^\Z^|Y|Xp GJ\PVPP&YH&QRDEXDEQXslixmpp-1.2.2/docs/_static/fonts/YanoneKaffeesatz-Light.ttf0000644000175000001440000037416012473141653024745 0ustar mathieuiusers00000000000000GPOS~qGSUB@Q'OS/2S``cmap C cvt &fpgmY7 sgaspg qglyfr4headc6hheaNT$hmtx" kern $#locaB~ maxpx namelZ\2post,nprepS8o&k_< |%`"18 89Zj s~,2@ KYN @ 89  " &B=$#&E&*-"+13 $$$1&'&4($1**'57a'8Q9G9$76.6I777&r7&{7q!o7 i/ o$:K()w&!6699P979(7p'*:d)rlb6[%&&;}+.a'Q9Q9Q9Q9)7&&&&&W/7777i1o$o$o$o$o$o$l$K(w&w&w&w&-7*::::rr9s$%q!p'il4A+%&&*%*&(&3-=+"*  gq- .+4h I==44=m=j=U,094 %sao/"+o4*:.:""O1\*.!7$%31(433.9r73#n&   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYjkmov{[`^a]l}\eZ_dfizbhpgqnsturxyw~cHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcdHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcd,K PXYD _^-, EiD`-,*!-, F%FRX#Y Id F had%F hadRX#eY/ SXi TX!@Yi TX!@eYY:-, F%FRX#Y F jad%F jadRX#Y/-,K &PXQXD@DY!! EPXD!YY-, EiD` E}iD`-,*-,K &SX@Y &SX#!#Y &SX#!#Y &SX#!#Y &SX#!@#Y &SX%EPX#!#!%E#!#!Y!YD- ,KSXED!!Y-+++dZF2$+^M<++nZF2$++ E}iDK`RXY27; B    &k+// /EX/ >YEX / >Y ++ и ии!и%и'и,и.01%#7##7#46;7#46;7>337>33+3#'37#:q@ 5I >qF :O qqO \O \|H UH U|.|Bo;d7+;/EX/>YEX/>YEX7/7>Y(+ и/01.54>732654&/.54675463.#"# -4-<6&p F?92 7&3<{$2%   ;1 6$i82>a y!&*r#@)5*^ =/?Q(8+0 +++EX/ >YEXF/F >YEXH/H >YEX+/+>YEX@/@>YEXB/B>Y+5014632#".732654.#"4632#".732654.#"&'.5=KB!2!HB 3#5 (/ (0KB!2!HB 3#4 '0 )0    $;L'\ aW*G5^[ (H7*5 BK*6 >WaW*G5^[ (H6*5 BK*6 > =a}@$M[ϸ\//и/$и$/)9и9/\AиA/TEX/ >YEX/ >YEX/ >YEX/ >YEX6/6>YEXY#$и<Y012&#">54&'>3:#'#"&'#".54>7.54>.'326AO2L) ,ET) 1#8+H  , +L98G) )$.-='T 0%-: "% boq0$DPm ,=* %*.) & ( 3B!9/% 23L I'47B?0E:%"+/EX/>Y01#".54>32} .2 &*#$+' 3,D`{S:XsVRlO5Om:%"+/EX/>Y014.'>32#"&'>,3 &+$#)& 3- DVoQ4PkSStX<Sy &//4>&7/&>&67'6.'    L  p a o0  2      *k?+ и//+и 01#5#46;54633#    -T+ /+0174632#"&'>7&7  +3+ * +3 +0146;#+  1U $+EX / >Y0174632#"&1( ) //01"#>3H  $X P//и/ EX / >Y+ 014632#"&732654&#"$VcaNQebP;8?E80CH9(rxv$Y%, + /EX/>Y 01.54>323:767#!&454>3N$,(  X  (64 )   $X-V &+EX/>Y+++9и/и/$901.54>32326767#!&5467>54.#"A.@'+8! 7R]% " "XO6$4S  "1:ece:  7a]]3 1X:7+ +1&++и/01#"&546732>54./>54.#".54>324'!;S2962$&=*-<AK#5J-?)*5 !,/#7':]B$6I*$- %$T/   !*4-&&\%,n&+ и&"/EX/>Y +и/и/ и&и"'01%:727+#5#.54>7>329   F !+24 : 63)   :JRNDyCKW]+'X.O%+и/ +. +*+ и/ и/01#*&#"'>36#"&546732>54.#"~ !(0;!"JA+7X?<9%G1C)!3>(9X  (M@ClM*  "%@V00YEX/>Y$++01"632#".54>32."32654& #>/2j!7(]a8C$ $?T/'-JX5 /%AE:#JrO[/J3|,QpEp_*   c7ZA#iTOL4X/+ и /01"&5>7""#"&5-SF8:CF /FX0 XJc b-(&7LM/5/и/Mи/"и"//5HиH/EX/ >YEX/>Y8*892C29012#".54>7.'.54>.'32654&">54.=3 #+#$WW1F-#-! />h#.#BA<@$W-" $+ "* #?3!5* D3Ti3E)!<3'  >!+@*k  *4>NK>.6u +1  $+"+ $b /Q0/$/и/0 и /$ -+*+!+012>7#".54632#"&546726754.#"2I2J6!8)]a5B% }~"2$U)G.'BG?3)NoG,/1J2~#KsP  306ZB%`bOK1 B+ иEX/>Y +014632#"&4632#"&1* 7+ и /и/// +014632#"&4632#"&'>7&2  3+ * * ++01463!#463!#* Q  Q X 9y:/+/:и/+и/#и#///EX / >YEX / >Y 00174632#"&2'&'&5467>54.#"&54>W83'%(  !  &!" 1!*(!7(,D90# ,127! &  5Cev !+++[J+n=+JEиE/JGиG/Jfиf/EX/>YEX2/2>YEX8/8>Y&+XM+Bi+2as014.#"3267#".54>32#"&'#".7>3265<76&#".547>3232>'.#"3267;]CDnL)(Ie=Vm' "@Z8.UK=,3]NKqJ%$#  7,/")6,%"2 ;09: %#  ' .XNoF &XjS!  $  0   3C#/ ?s 0$"*!lEX / >YEX/>YEX/>YEX/>YEX/>Y+ 01%##*'632#*/3'.'B.     0   P;4**4;7q$1o2/+/2и/+$%EX/ >YEX/>Y1+#%0146;2+32654.+32>54.+7 d<0")%'9Dz7K+<&GC.$"0C !;0,<& 8.K]4;J", *G5+1'I)C%+EX/ >YEX / >Y  012.#"3267#".54>1,4)!+#8 !-YEX / >Y 012+4634.+32>0I05I*y "3"EC2&!IxWvV  Hb=H95f+EX/ >YEX / >YEX/>Y+ 01%+47>;+3+4  3#" 95m+ EX/ >YEX/ >YEX/>YEX/>Y + 01347>;+3+#9  " $g*k+//и/+&и&/ EX/ >YEX!/!>Y+!012.#"3267#54;#".54>.;*'=)#0$)b!.,F08N Gz^cv>  "  LnsQ7y//и/ ииEX / >YEX/ >YEX/>YEX/>Y+01#"#4>2334>3"#B    cQ6n /+EX/ >YEX / >Y014>3"#6   Q5+EX/ >YEX / >Y014>3#"&546732>5 1*31+ !@2" '46z${+EX/ >YEX/ >YEX / >YEX/>YEX/>YEX$/$>Y014>23>32#*'.'.'"#6  ! ?h$  %%&   .) EhN7 75 5+EX / >YEX/>Y0173+4>3o 4' 72{3//3и/'и/EX/ >YEX/ >YEX/>YEX2/2>Y0146;67>7>;"#+'.'"#7%m  i*  K:8   (&T Q! 7KRKTL712/(/2и/и/и/( и /(-(1и1/EX/ >YEX-/- >YEX/>YEX0/0>Y01.'"#4;.4&4=4>23+  'x  &'8BA;=GJ>,*:CA9;HLB/Q &'](//(и/ EX/ >YEX/>Y#014>32#".732>54.#"&0H01D*.H10E+= /!"1 /!!1caT&$SdeW&"TYPnF!JvUSqF!Ky7[e//и/ EX/ >YEX/>Y +014;2+"#4.+327 h^Q0B';  * D?sfX9Q42$&z#78/./8и/. и/$EX/ >YEX/>YEX/>Y+)3014>32327#"&'.732>54.#"&0H01D*#5$%-5-?)= /!"1 /!!1caT&$SdXW0 0# :B&TVPnF!JvUSqF!Ky7[&01/'/1и/и/' и/'и- EX/ >YEX/ >YEX/>YEX/>YEX&/&>Y/+,0147>;2*'.'"+"#4.+327h^Q70    ;  * D?sfXU`3I`< DfH, 2%!S0}1/%/1.и./ %.и/EX/ >YEX/>Y""9*9012.#"#".546732654./.546C; 7(4>w**B/#1!5+A7l""W .&2{-N1'C1  D6%$%q&B&;LhA+EX / >YEX/>Y и01"##543!#  T &' 7`//и/EX/ >YEX/ >YEX/>Y01732674>23#".54>23o2*"? ]7>G&  #?. p] %=R-GEX/ >YEX/ >YEX/>YEX/>Y01&6;>7>;#"&'.'& ,oe -    J=i..i= >!Wel3y=iEX/ >YEX!/! >YEX#/#>YEX&/&>YEX1/1>YEX4/4>Y01&6;3>7>;3>7>;#"&'.'#"&'.'& ,\ M 'X  R -  K N    I3.+4]1 n3-+(e4 >D .78  Xdk3x )XEX/ >YEX)/) >YEX/>YEX/>YEX/>Y01+'.'+&>23>?>23 9D  'D 0 3  0 k 1+$ [2kGy2-% $-3od@+EX/ >YEX/ >YEX/>Y01&>23>?>23"# 0  ,  GA22BFbpMEX/ >YEX/>Y 990143!3267#!&547#"2*   +  Y:52 +EX / >Y+ 01+3+463 5!k  ' //01#&63$%'   8 62 +EX/>Y +0146;#4>;#  !  n +01463!# P ^G //01".'>32)3, !G&"   $A&;YEX/>Y*+"40147>32#".54>3254.#"&.#"3267>$W7<<B54(*6- 0L%(#* 5DYEX/>YEX/>Y!01463>32#"'732>54.#":!;*1"&:DM&7 -' $2-I6nD& 7hW-<$(0$C +EX"/" >YEX/>Y"01.#"3267#".54>320 (,$(0 ?3"7'!5@#1  /YK=M+ 5bQZk9)Y$i%//%и/и/EX"/" >YEX/>Y" 01.#"3267463#".54>32!,"(*!H-!;,-="3 4T=BP* 5 3aSHhB &W$45/2/5 и / и/ *и*/2/и//EX/ >YEX/>Y* +%012#3267#".54>":>7>5.B< FA8+"?%2$<+!3@(!/65SK ?7C$    2\K`r<2 *OB!<;T(+"и'EX/ >YEX/ >YEX&/&>YEX / >Y +!и"и(01<>;54>32.#"3++=/:-)#(!hZ ) ?:F%  2*A I !;=M`a/^/#и/#и/a-и-/TEX/ >YEX/ >YEX(/(>Y (YNܹ>F0123#"&'#"'#".5467.7467.54>"32>54&.'32>54& | '>+#'51$0I22>" 2 $%%-)!& 4+ -*""2!: =.'C1 !   '6*&%B 0R0/D-+AH0$(7 >8@:  )6_!}"//"и/ EX/ >YEX/>YEX/>YEX!/!>Y01463>32"#4.#""#6!C$,  ; "2l"k6w&6%EX / >YEX/>Y01=~&=+EX/ >YEX/>Y 019i2=y+,и89/EX/ >YEX/>YEX/>YEX2/2>Y9*501463>32"#>54'."#4#">9!O 1'"# '"  !#   3=#>.0("5'$7&/ &3# &/9p /+EX / >YEX/>Y017"#463p ! 91 +*+#+EX/ >YEX/ >YEX/>YEX#/#>YEX-/->YEX1/1>Y'и'/01.#""#>32>32"#4&#"#"#, $K%%. D %, *. V%6#p1+ \ 7bh//и/ EX/ >YEX/>YEX/>Y01>32"#4.#""#7#U*(5   < "4#f' Wa#]$//$и/EX/ >YEX / >Y0174632#".732>54.#"YN':'VP$:); $& $'z;dM}7fQAQ-3Q;AQ.2S9;n&'//'и/&EX/ >YEX/>YEX/>YEX / >Y$01632#"&'#"&532>54.#"9AO&=+!29 /  7()%(1%(0R=fzA 3bT4C'(:_%&//&и/иEX/ >YEX/>YEX/>YEX / >Y "01#"#".54>32.#"367_  $L2*6J,#38 #>D$551[LMnF $ qYEX/>Y 01>32.#""#7 E!%# 1%  `'K+,/"/и/,)и)/ "и/)и/ &и&/EX/ >YEX/>Y 9% 9012.#"#"&54732654&/.546#1 3 .;l#!WM:D 3#2:i)W !  M9&BE %(-L25=%t +и  /EX/ >YEX / >YEX / >Y иии 01#46;54633+3267#".5eJ =#ym $!!& )9":e\//и/EX/ >YEX/ >YEX/>Y01%#".54>2332674>23e!Q&,9!  &4 ,C0=#/ LGEX/ >YEX/ >YEX/>YEX/>Y01&>3367>?>23#"&' +    '  -^&-**-&_-QEX/ >YEX(/( >YEX///>YEX1/1>YEX4/4>YEXC/C>YEXE/E>YEXH/H>Y01&>233>?>;3>?>231#"#"&/.'#"#"&'.'& (  # $+  "         0X#)%3JR)y?HK$T/GEX/ >YEX/// >YEX/>YEX/>Y01"#'.'"#'&>3>?>23 4  -  3  -  P*(! !'+DP#!!$D?W(<EX/ >YEX/ >YEX/>Y%01&>34>7>3#"&54673267! K  C  "($% #/3,+5.>&8IN${EX/ >YEX/ >YEX/>YEX/>Y  к 9 9013267#!&'&547#"&4543Ht08 y%: o  8/ "+ /EX/>Y01"#4>3  6: I+и//EX / >YEX/>Y 01#"&54632#>3 * f  [tc~1m(+"/1/EX/ >YEX-/- >YEX/>YEX#/#>Y 01.#"3267#5.54>75463, -&+-"-(3  1\N@J%   j 2YGOh?j %60+и0*EX/ >YEX#/# >YEX6/6>Y+ + $и(и+и/017#46;5'#46;&>234>?>233+3+"#qsKo 0  . ob\o :90E>..>F5>`  +и 014632#"&74632#"&&@%5GI6&+ +.@+;1+)C++#+01.#"3267#".54>324632#".732>54&#"#    "  !k]/G/e^.H2,&6#"9)KE#:)-&"/)1: zsi3T>lq2U>2C(-C/bN+Ev%K%EX/ >YEX/>Y0174>7672#&'.34>7672#&'. !*   $%   ,! !)   #$   +! $+3> !&-58.&!  @5-&")09 #)21(#  80("&@!?Gi+D"+*@++*/и//@5и5/D; ++F:+014632#".732>54&#"7476;2"&#.'##74&+32&k]/G/e^.H2,&6#"9)KE#:)Z1-&)   !i3si3T>lq2U>2C(-C/bN+ES)7 4/'faB //01"&'&'>32.( ).B  +! ";: + +014632#"&; ?}$9EX / >YEX/>Y+ 017272632#"'&'67>7632654&#"7G , ?)$  a+%KGEX/ >YEX/ >YEX/>YEX / >Y01%&'.54>767&'.546767&'.54>7672.546767 "+   &%   )! !+   %!  )! &-5@ !'.85-&! >3+$ #(08 #(1)0-# 90)"= ;uYEX / >Y  101#"&54632".54>7>54&7>326783'%(   &"" 0!+[!7(+E90# ,027! '  & >f& a& _|& Jz& ]m& g%+иEX/ >YEX/>YEX / >YEX / >Y++01%+5##*'>;+3+'3f W  H  3#" 676'?IMI+EX/ >YEX,/,>YEX./.>YEX / >YEXD/D>Y&>+  ,8012.#"3267272632#"'&'67>7632654&#"7.54>1,4)!+#8 ( , 4($8E#  HqVi9 2)$  TF~hR95&$>O9;&$a98&$G95z&$]U&(>)&(a)&(z&(]7|&-]&&.>u&&.a&&.m&|&.X&z&.]{WX7&'7'>76'R]^ST_^]ik ^^ ki/):FG/*/G и /и/*и/*!и/ ;)и)/EX/ >YEX/ >YEX/>YEX&/&>Y6B01&'4>7.54>327#"&'4&'32>'.#"Y /H0&9   .G1&; !$%"2!1! &!1   *caT&5 *[eW&BJj#U`dXG#FyIHh#!Ky7&4>x7&4a7&4p7z&4]~d&8a1Fy9F+,+%+EX/ >YEX/>YEX=/=>YEX?/?>Y"6014632#"&546732654.54>54.#"#"'&'67>59VG6J."'.'QE:< ,#+:'.'(1( 2#82 %TG*2! !$.:%FK  -00)$&)"*!"!>J $A&?>F$A&?a$A&??$G&?*$A&?]M$A&?_$?J[lS7+.[+j+7и/. и /&и&/[?иjgиg/EX/ >YEX/ >YEX+/+>YEX2/2>Y+ E XиE\01467>32>32#"&'3267#"&'#".54>3254.#"..#"3267"3267>5.(K>05 $'B<.0M)$@%2&>@54(*5-  +E%("/ %!K(   #,PK B =G$     =55A$ T 1)$)  %E9 $<8(?0H A+EXF/F >YEX$/$>YEX&/&>YEX/>YEXY6+F$001.#"3267272632#"'&'67>7632654&#"7.54>320 (,$(0 5, , 0"!5@#1  /YK=M+ 3)$  U6_KZk9&W&C>^&W&Ca&W&CW&W&C]e&>-&a-&&]7b&LEa&M>Ra&Maa&MJa&M5a&M]X*c;+и +++01463!#4632#"&4632#"&* Q u) )9E~F/*/F и /и/*!и/ :EX/ >YEX/ >YEX$/$>Y5A01.54>7.54632>7#"&'4&'32>'.#"<  WN$7    UL(< 12, && %'  P9z#  R8}&(<SUJ3Q4#8!2S:e&S>`:e&Sa:e&SX:e&S]f?W&Wa?W&W]S9r /+EX / >YEX/>Y017"#463r ! $X';YEX/ >YEX/>Y"%+-и-/01%+5#".54>32547>;+3+32>54.#"W C/0E,/H0-> /!"/ ,!!1 3#A'##SiaU'#)" PoG "KwUSrG "Lz%`0DT1+J;+R+Jи и /ROиO/EX/ >YEX / >YEX%/%>YEX,/,>YJ+%6и@E0174632>32#327#".'#".732>54.#"%":>7>5.%XN0A "')B< FA8)L0%2*$ )W&<); $& $'}(!056z$/!PK B:D" "   N7fQAQ-3Q;AQ.1S )PD$<8!S&2Q'K&QSdz&8]Lp&9_N&XD4T9+EX/>YEX/>Y +014632.#"#"&'&'67>5;WN4@ 2#94 KTC 6E  C// //01632#&'.'#"&'o "D///01".'>32>767{&)# &$D )$ ! (+"oA +01432#"&! ./>  ++014632#"&732654&#"2,,*0-).+0+'3.-&2L%#+й и /01>3232>32#".#"#"&'&4"   p)   +3 +01463!#+ Q 3 +01463!#  %Cy+ //01#"&54>32v l92! > &>z+ //014632#"&'>7&) 92! > &zd+ //0174632#"&'>7&) ;91" = %C%G&//&и/и/!и!/ ////01#"&54>32#"&54>32v  l92! > 92! > &>%K&//и/&и/!и!/ ////014632#"&'>7&'4632#"&'>7&  92! > 92! > &d%K&//и/&и/!и!/ ////0174632#"&'>7&'4632#"&'>7&  ;91" = 91" = 3[ + +014632#"&3  " '%%$-U&'5=!1AQaqbR+Zj+B2+:J+"+*+"и/EX/ >YEX/ >YEX/ >YEX/>YEX/>YEX=/=>YEX]/]>Y5O+/=G5UиGgиOo01&'.54632#".732654.#"4632#".732654.#"4632#".732654.#"    $;L'\KB!2!HB 3#5 (/ (0KB!2!GB!3#5 (0 )0KC!2!HB 3#5 '0 )0  =b|AWaW*G5^[ (H7*5 BK*6 >V`X*G6^[ (I7*5 BK*6 >M`X*G6^[ (I7*5 BK*6 >%%EX/ >YEX/>Y0174>7672#&'. !*   $%   ,! %,4? !'.76-&!  >4,&+%GEX/ >YEX/ >YEX/>YEX / >Y017&'.54>767&'.546767 !+   &%   )! &,4>  &-77.'! ?4,%L %EX/ >YEX/>Y01##"&'632D   ^C7+и/и/72и2/7:и:/EX-/->Y +++-"2и6и:и>012.#"3+3+3267#".'#46;547#46;>/$ *)% n!(6 ) :-[B\J (29^   =4&-8B"   .WI-&CR,"1C36+-1++и/-(и(//1/6/EX/ >YEX/ >YEX/ >YEX;/; >YEX=/= >Y27и801476;67>?>;#74>5+/###5476;#20  !# m ]  r"$ 0V[J "$g*3 +01463!#* Q )5CD/4/Dи/и/и4/&и/1и1/6и47EX / >YEX'/' >YEX6/6 >YEX/>YEX/>YEX1/1>YEX3/3>Y!+6иии/-и.и5и!>и>/01+#<>;54>32>32.#"3++'35467.#")L="066?V$)!h[) $ !I J9@  # 2*A I0? 2 -' f 12//2и/#*и#-и-/0/EX / >YEX/ >YEX#/# >YEX/>YEX-/->YEX///>Y )и*и101%"#463<>;54>32&#"3++U !<2;06)5)!TF) 0 ?=F$ 3,> I ^-Ѹ.//.и/и/&и)и)/,EX/ >YEX/ >YEX/>YEX/>YEX)/)>YEX+/+>Y +%и&и-01<>;54>32+.#"3++ <-4":)"dV* ?=F#  G1,E I>6CMB+(-+DI+(иB/и5и-7EX/ >YEX / >YEXB/B >YEXM/M >YEX*/*>YEX,/,>YEX2/2>YEX4/4>YEXI/I>Y+ и /&и'и.и/и6и<и;54>32>32&#"3++#+747.#"3"#463<"156A 05)5)!TF)*% "   J9@  3,> IIo?& -'L7=J++$+и$,и>иHEX/ >YEX+/+ >YEXI/I >YEX/>YEX/>YEX/>YEX!/!>YEX#/#>Y8 +ии%и&и82и2/ CиC/01%"#.#"3++#+#<>;54>32>32347.#"37  #TF)*L<"1549*8$ " 0*G II J9@    >' -'Lx&RX*-8a$56/%/и/6и/%-EX/ >YEX/>YEX/>Y (201"&546732>=#".54>32.#"3267)9%2ZG:K+"# )+"и(EX/ >YEX/ >YEX%/%>YEX'/'>Y +!и"и)01<>;54>32.#"3++ <'#   hZ) y%. s I.CW-+/C/2++'(+и/'$и$/?и?/017.5467.54675463.#"326732>7#\PA/"]I#5#G1Yd+KZ+;/014&#"26767>>7675463>76732767#".5#32>7#".54>32#& (#; ,*     (/# ?>6x1(($6$&<*+F2*"A1/RAOd !+    ">3AH# 4^KHlI% !9- 4yHI/A/Iи/и и / и /!иA.EX"/" >YEX/>YEX/>Y+>3+"01#"&'&'67>54632.#"!#".546732654&'.'7r XP)2#:3J8,2H0 * '$@K-$2\ T@ 9I8 ':'0R<"   ZM46 yM)2"+EX/ >Y+01463!#".546732654&'.'78+1I/ + '#AK.#2! ':'0R<"   ZM46 y78/0/8и/ ии0/EX / >YEX/ >YEX/>Y-"+и01##46;5463!#".546732654&'.'7 _ R"C8+/G0' % @F.#2Q! ':'0R<"   ZM46  w()//) и /и #EX/ >YEX / >Y +и !и #012+#46;4634.+3+32>0I05I*y,  "3"Eh ZC2& JvUxW X" Hb=H5b +и //EX/ >YEX / >YEX / >Y0173+.?4>3of g) ) !"'G  , G +и  //EX/>YEX / >Y017"#.?463p3 3 2 2!>R=![A +EX / >YEX / >Y+01&547>323#!&546;G-+# _  qp    4=a9T!1+EX/ >Y.$+7+и/74и4/014632>54.#".7>32#"&'&54732654&'.#. "$; E6". 8#&`M+9 1'5?&- &    =, 3"FE  ,'$4!^+2 $+EX/ >Y+)01.54>323#!.5467>54.#"L*5%/ )=F"I=')6b   (&D?=   %>;9  4v$i +и и/ / /EX/ >YEX/ >Y+ ии/013+#"'5#&547>7>327+F :  &04   3*)"   <L  KKC 3>D=S&RS/K/Sи/ K4?и?/EX / >YEX / >YEX$/$ >YEX/>YEX/>YEX?/?>Y+/P+и?901&547>323#!&546;#"&'632.54>323#!.5467>54.#"G-+# _  qb r*5%/ )=F"I=')6p    4x     (&D?=   %>;9  =0&'g=-&'mU; q+и////EX/>YEX/>YEX / >YEX / >Y01#"&'4632#"&'4632    ]  k  ,nb ///01#"&/&547%t   hn  0ge// /01'4632.547   hn  9r/ //017#"&'7'i     i4j 7;!+и!3+++'+01#"&54654&'&547>54&54> ,4   &KB! )9$1&_<% '->`,"(=I4`;)"   9d0!, j=;6 + и6)+!+,1+01.547>54&54>7.54654.'.54632 -4   ' KB! )9$1&`<$ (->_-"'  >H125)"    9d0 -  E! + и/01>3232676#".#"& '!' '$        % )+и/%EX/ >YEX/ >YEX / >YEX / >YEX/ >YEX"/">Y 01&632'76'.7&547#"&'>   ~    >  -  D a2 ?//и/ ++014632#".732654&#"a2,,*0-! )0+'3-.#ZS+h +и/ и&EX / >Y!+'+  и'01+.5#46;4>32.#"3#%I <*:#% %) 9p6 MBOg=   -RA/@DVW/K/и/и/Kи/W;и;/%и%/;(и(/;T?EX/ >YEX"/">Y "-012.#"#"&'&454732654&'.'.5467'.546>54&'.'08 - */ !$"  PB09 - +. ## " P// !  + 7 &1;!  +8 &1;z% *"% +!+#0++$*+и/EX/ >YEX/>YEX/>YEX'/'>YEX*/*>Y01%#"'5#".54632.#"367#"&'4632e  4 =/i]'67"AF$-.    5YE $ ws:J*  v!YEX/ >YEX/>Y+ +и ии 017#"'7#46;7#46;76323+3#0-f l. 1/l p- |||4w; 3DkE/,/Eи/,4)+ 1+ ии/)9и9/1@и@/014>32>32&'.'#".%32654&'&3267'.'&4 4') ;19D@9  #5>+!0? $. &. +  2)!$6HFIH1##7'4O !.0"+A!5$ 1 0* I+ и//++и 01#5#46;54633#463!#   Q   :=g!"//" и /EX/ >YEX/ >YEX/ >YEX!/! >YEX / >YEX / >YEX/>Y01%#"'#"&'463232674632gQ*C   5*0  $ 0(  .'z(// (и/и/ EX/ >YEX/>YEX/>Y#012.'>32#"&5464&'&#"32>E)TF `[ %E7ZZZ*E?AA;(0-Ox5  I֞;mR1{}/Fobpc+F\:BR9EX/ >YEX/>Y  01!+3#:   G "?//и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yи ии01#"&'##"&'#&5463!#Y    4 [  D D   "//и/ EX/ >YEX/>YEX/>YEX / >YEX/>Yиии01#"'##"&'#&5463!#Y    4 [ Q Q    <&2+EX/>Y +!014>32.#"#"&546732>5/:++#(!/:,+$)!8Y2 +(+01>32#".54>3254.#"..#"3267<;0&C)) +&  4 !  %   %0(%0 N ! *"2P // и/EX/ >Y +014632#".732654.#"*L>0FB/!0 */ *1pd0Q?kf.Q<0=# MV0>$I.56//6!и!/!и/!и/)/и//)2и2/EX$/$ >YEX/>YEX/>Y$ .и/01!>54.#"#.546;.546323#". 1!" A ^c0A( R J\j9TrGCpS9m`M  ;Rg>$P}XBkR:  !ke)'+//+01%#"&=!.5463!e   0o v  7?@EX/ >YEX/>YEX/>Y017>323+#"&'7 Of Un   <2z+и-EX/ >YEX./. >YEX/>Y (+.и01#"&546732>5#46;54>32.#"3# /:,+$)! {/:++#(! $=D! /*D3232676#".#"&>3232676#".#"&% ( 7)"%!- '"' A   # q       <EX/ >YEX/ >YEX/>Y01>32!7!#  x@  H48$%EX/ >YEX/>Y01'7Ȍcc%&0;Eû &+&и/ 'и'/&,EX / >YEX/ >YEX/ >YEX/ >YEX"/" >YEX*/*>Y<1+и<7и7/1AиA/0146'&632'76'.7&547#"'7>&546776'.7    ~    =  ~   ~   -  G ,  1)f%EX/ >YEX / >Y012#".54>">54&#".2#"&#"3263232654#"#"&=463232654&#"#"&#"3262654&#"#"&#";2/.#"#"&#"&#"#"&#";27'32654&#"#"&#"32632#"&#"3263232654&#"#"=7632326732632#"&#"3263232654#"#"&=326=463R^33^RR^33^[#&-.&$" #     k    F       "   :   :      3^RR^33^RR^3!&"%%"&!' p     p         b   " 7   ^"   $[ q    a) v g +01.5>;# g   D/+0132>7>#".546  ),  ' #% 47/// ///01>32632#"'#"&'  p   =EX/>Y 0176327#"&54>Z   #*     &++#3n& ///EX/>Y01#"/&547%463!#{     & gn  z3i&///EX/>Y01'>32.547463!#!    &hn  .&67/'/7 и /'#и'&и&/'*и*/ /EX/ >YEX/>YEX/>Y,201#"&54632&'.?.'>3274&'&#"32>.20 %E7ZZZQE)7`  B   M  *E?AA;(0Bs;mR1{}-ZF&  /Fobpc+F\98n)*/"/*и/и"EX/ >YEX/>YEX/>YEX/>YEX / >Y'01632#"&'#"&546332>54.#"p'2&=+$5: *  ! $+'(3% 1S?dw@ vn4bR5C&7['y(//(и/ и EX/ >YEX/>Y&++ и /0146;32+"#4.#"#:2327?/C*0B';  * s 1G,9P42$3 E+и//EX/ >YEX / >Y0174632#"&463#3 * ( =w5+EX/ >YEX/>Y 01#"&54732>5463w 0&67 / !%":* ,#H)w*/ /* и /и/  и /  и/ и/ % // /%/////01"+.547>23"+.547>23L q I'47B?0'47B?0 2p.Fn(~fL  N x   p L V 4bPl2d&n.~FP h@lbBV f !.!!""# #T#$^$%%@%&&&&&&&&''~(.(:(F(R(^(j(v((((((((())))))*******+,x,,,,,,,,,,,--- -n.."...:.F.R.^./,/0000&020~001121|11122.223L3l3|445B5l66678l9::;;<<=l>">z???@.@z@APABBBCC>ChCCDlDEBEEFGPGHLHI"IIJRJKKKLLMMMN2N2NZO,PPPQQVQQRrRSrSSTp%` B    "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-    " & . 0 9 : ; <9 ? A B C H K L M O P R S T U V W X [ _ f g h i j k l m t7 u w x y z { }             ){ . 3 7 9 A B C E M O f g h i j k l    " & . 0 3 4 5 6 8 ; A B C D H5 M O R S T U W m w x y z { } ~    )2356789;DRTUVWXdfghijkl "&.034568;ABCDMORSTUWmwxyz{}~& ) \\  !"&).0356789:;<=s?ABCDEGHKLMNOPQRSTUVWXZ[_bdefghijkllmpqtuvwyz{|}1+&6O&&OO&)35678;<=  = ==  )7=C?BCEfghijkpqvyz{==353HU  )fghijkpqvyz{ )3578HCV   $  " & . 0 3 5 6 8 ; @ A B C D M O R S T U W _ m w x y z { } !! ! !! !)!3!5!7!8!=!R!T!U!V!W!f!g!h!i!j!k!l!p!q!v!y!z!{!!!!!!!!!!!!!!!!!!!!!!!"" " """"""""&"."0"2"?"A"B"C"D"M"O"R"S"T"U"V"W"X"_"m"w"x"y"z"{"}"""""""""""""""""""""""""""""""""""""""""""""""""## # ## #)#3#5#6#8#=#?#M#O#V#f#g#h#i#j#k#l#p#q#v#y#z#{#####################################$ $$ $$?$A$B$C$M$O$P$S$U$V$W$X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ $ $ $ % % y%%y%%%% %)%=l%?%A%B%C%E%M%O%P%R%S%T%U%V%W%X%_%d%f%g%h%i%j%k%l%p%q%v%y%z%{%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %y% % %y%y%%% & & &8&T&V&W&&&&' () ) )) ) )))=)V)f)g)h)i)j)k)l)p)q)v)y)z){)))))))** ***"*)*.*0*?*A*B*C*D*M*O*R*S*T*U*W*_*m*w*x*y*z*{*}********************************************+n+ + l+++++"+&+.+0+2+3+5+6+8+?+A+B+C+D+M+O+P+R+S+T+U+W+_+m+w+x+y+z+{+}++++++++++++++++++++++++++++++++++++n+n+n+n++++++++n.. . . .. .)...3.5.6.7.8.=.A.V.f.g.h.i.j.k.l.p.q.v.w.x.y.z.{.}................/ / {//{/// /)/7/8/9/=3/?/A/B/C/M/O/V/_/b/f/g/h/i/j/k/lc/p/q/v/y/z/{////////////////////////////////////{/{/{//00 0 00 0)030507080=0V0f0g0h0i0j0k0l0p0q0v0y0z0{0000000000000011"1.13151617181B1C1M1O1V1W1l1m1w1x1y1z1{1}1111111111111111122)2=2D2T2U2V2W2l222222222222233 3 33333333 3"3&3)3.303=3?3A3B3C3D3E3F3G3H3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3_3d3f3g3h3i3j3k3ly3m3p3q3v3w3x3y3z3{3}333333333333333 333333333333333333333333333333333333333333335 5 5555555 5"5&5)5.505=5?5A5B5C5D5E5G5H5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5_5d5f5g5h5i5j5k5l5m5p5q5v5w5x5y5z5{5}55555555555555555555555555555555555555555 5 55 5 55555555555 66 6666666 6"6&6)6.60696=6?6A6B6C6E6K6L6M6N6O6P6Q6S6T6U6V6W6X6_6d6f6g6h6i6j6k6l6m6p6q6v6w6x6y6z6{6}666666666666666A6666666666666666666666666666666667 7 7777"7&7.707?7A7B7C7D7M7N7O7R7S7T7U7W7_7l7m7w7x7y7z7{7}777777777777777777777777777777777 7 7 7 77777777 88 8888888 8"8&8)8.80828=8?8A8B8C8D8E8G8H8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8_8d8f8g8h8i8j8k8m8p8q8v8w8x8y8z8{8}888888888888888888888888888888888888888888888888888888899 999?9A9B9C9D9E9M9N9O9P9R9S9U9V9W9_9999999999999999999999999999999999999999999: 1:::H8;; ; %;%;; ,;f,;g,;h,;i,;j,;k,;p,;q,;v,;y,;z,;{,;,;,;,;,;;;%;;;%;%;< =-==o====="=&=.=0=3=5=6=8=A=B=C=D=E=H)=M=O=R=S=T=U=W?? ?D?R?V?W??????????????@@ @ @@@=@R@U@X@@@@@@@@@AA AAAAABACAMAOARAUAWA_AAAAAAAAAAAAAAAAAAAAAAB BBBCC C CCCRCUCVCCCCCCCCCDDD rD D DDD DsD;oD<D=D@DADBDCDEDFDKDMDNDODPDSDU D^?D_D`>DDDDDDDDDDDDDDDDDDDDDDDDDDDDeDDDDDDDDDNDEEEAEBECEHlEMEOEVEWEXEEEEEEEEEEEEEEEEEEEEEFFFRFUFWFFFFFFFFGHHH?II IIIAICIMINIOIUIWIIIIIIIIIIIIIIIIIIIIKK KKDKUKVKWKKKKKKKKKKKKKLL LLVLLLLLMM M M MMM=MDMRMTMUMVMWMXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNN N N NN NN=N?NDNHNRNTNUNVNWNXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNOOOOOOOPP P PPPP=PAPBPCPEPMPNPOPPPVP_PPPPPPPPPPPPPPPPPPPPPPPPQQ QQQVQWQQQQQQQQQRR R RRRRRARBRCRERMRORRRSR_RRRRRRRRRRRRRRRRRRRRRRRRRRRSS SSSSSSTT T TTTTTTT=T?TATBTCTETITKTMTNTOTPTQTRTSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUU U UUUUUU=U?UAUBUCUEUIUKUMUNUOUPUQURUSUVUXUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUVV VVVVV?VAVBVCVDVEVIVKVMVNVOVPVQVRVSVUVVVWVXVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWW W WWWWW=W?WAWBWCWEWIWKWMWNWOWPWQWRWSWVWXWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXX XXAXBXCXEXMXNXOXRXSXUXVXWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ"ZH^[[ [[[[[\^_3_5_6_8`d dd)d3d5d6d7d8d9dTdUdVdWdXddee e eHwefegeheiejekepeqeveyeze{eeeeeeeeeff f ffff"f&f.f0f3f5f6f8f;f@fAfBfCfDfMfOfQfRfSfTfUfWf_fmfwfxfyfzf{f}ffffffffffffffffffffffffffffffffffffffffgg g gggg"g&g.g0g3g5g6g8g;g@gAgBgCgDgMgOgQgRgSgTgUgWg_gmgwgxgygzg{g}gggggggggggggggggggggggggggggggggggggggghh h hhhh"h&h.h0h3h5h6h8h;h@hAhBhChDhMhOhQhRhShThUhWh_hmhwhxhyhzh{h}hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhii i iiii"i&i.i0i3i5i6i8i;i@iAiBiCiDiMiOiQiRiSiTiUiWi_imiwixiyizi{i}iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijj j jjjj"j&j.j0j3j5j6j8j;j@jAjBjCjDjMjOjQjRjSjTjUjWj_jmjwjxjyjzj{j}jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjkk k kkkk"k&k.k0k3k5k6k8k;k@kAkBkCkDkMkOkQkRkSkTkUkWk_kmkwkxkykzk{k}kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkl ll?lAlBlClElMlOlPlSlUlVlWlXllllllllllllllllllllllllllllllll l l l l mm mmm"m&m.m0m2m?mAmBmCmDmMmOmRmSmTmUmVmWmXmmmwmxmymzm{m}mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmn nn?nAnBnCnMnOnPnSnUnVnWnXnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn n n n n o oo?oAoBoCoMoOoPoSoUoVoWoXoooooooooooooooooooooooooooooooo o o o o p pppp"p&p.p0p3p5p6p8p;p?p@pApBpCpDpMpOpPpQpRpSpTpUpVpWpXp_pmpwpxpypzp{p}ppppppppppppppppppppppppppppppppppp p p p pppppppppppp q qq"qq"q&q.q0q3q5q6q8q;q?q@qAqBqCqDqMqOqPqQqRqSqTqUqVqWqXq_qmqwqxqyqzq{q}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq q q q qqqqqqqqqqqq s t1t 8t1t1t1t1t1u u u u u u u vv"vv"v&v.v0v3v5v6v8v;v@vAvBvCvDvMvOvQvRvSvTvUvWv_vmvwvxvyvzv{v}vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvww w ww w)w.w3w5w6w7w8wAwVwfwgwhwiwjwkwlwpwqwvwwwxwywzw{w}wwwwwwwwwwwwwwwwxx x xx x)x.x3x5x6x7x8xAxVxfxgxhxixjxkxlxpxqxvxwxxxyxzx{x}xxxxxxxxxxxxxxxxyy y yyyy y"y&y)y.y0y3y5y6y7y8y;y@yAyByCyDyMyOyQyRySyTyUyVyWy_yfygyhyiyjykylymypyqyvywyxyyyzy{y}yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzz z zzzz z"z&z)z.z0z3z5z6z7z8z;z@zAzBzCzDzMzOzQzRzSzTzUzVzWz_zfzgzhzizjzkzlzmzpzqzvzwzxzyzzz{z}zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz{{ { {{{{ {"{&{){.{0{3{5{6{7{8{;{@{A{B{C{D{M{O{Q{R{S{T{U{V{W{_{f{g{h{i{j{k{l{m{p{q{v{w{x{y{z{{{}{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{|}} } }}}} })}.}3}5}6}7}8}?}A}B}C}E}F}I}N}O}P}Q}R}S}U}V}W}X}f}g}h}i}j}k}l}p}q}v}w}x}y}z}{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}"&.03568;@ABCDMOQRSTUW_mwxyz{}"&.03568;@ABCDMOQRSTUW_mwxyz{}  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}  DRVW DRVW DRVW DVW DRVWDRVW DRVWX ABCMORUW C CRUV CRUVW CRUVW ) F))))),FVW,,,,,5 F VW55555 VW ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DIRTUVWX ?DERUVW   VW VW ?ABCEIKMNOPQRSVX ?ABCEIKMNOPQRSVWX  ).35678?ABCEMOPSUVWXfghijklpqvwxyz{}   ?DIVWX"&).03568;@ABCDMOQRSTUVW_lmwxyz{}VW  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}"&.03568;?@ABCDEMNOPQRSTUVW_mwxyz{} ?ABCEIKMNORSUVW )2356789;DRTUVWXdfghijklpqvyz{ )2356789;DRTUVWXdfghijklpqvyz{   "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-   "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-"&.034568;ABCDHgMORSTUWmwxyz{}~   "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-   "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-"&.034568;ABCDHbMORSTUWmwxyz{}~"&.034568;ABCDMORSTUWmwxyz{}~358 )35678DRUVWXfghijkpqvyz{  r   [;o<@ABCDEFKMNOPSU ^?`>eNVW r   [;o<@ABCDEFKMNOPSU ^?`>eN       "&).035 6 8;"?ABCDEKLMNOPQRSTUVWX[efghijkl/mpqt%u vwxyz{}2+-!  -:*Pz  u   "3Uo  .F t @ .  ,  T6     8 8 D 4@ "t  Copyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone KaffeesatzLightYN: YanoneKaffeesatz-Light: 2010Yanone Kaffeesatz LightVersion 1.002YanoneKaffeesatz-LightYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone Kaffeesatz LightCopyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone Kaffeesatz LightRegularYN: YanoneKaffeesatz-Light: 2010Yanone Kaffeesatz LightVersion 1.002YanoneKaffeesatz-LightYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone KaffeesatzLight2  "#$%&'()*+,-./0123456789:;<=>?@BCDEFGHIJKLMNOPQRSTUVWXYZ[\]_bcdefghjikmlnoqprsutvwxzy{}|~     !A^`a.nullperiodcenteredlongsEuroffffifflt_zg.ss01f.ss02ampersand.ss03ampersand.ss04germandbls.ss05z.ss05t_z.ss05uni2074Deltauni00A0macronuni0237` :latncpspkernPFPk~ ~\&*~  4 ~ H X.X8L$dvV$Vh* `!!"#$#*#8#>#D#J#T#$l%6&&'(^()*+,- ---(-:../01(2:2@3>4466686j667727|7788@8j8|88889r::;4;<\>"?$?@@ABCtD2DE.FGH|IK(KLpL~LLM,MMMNNNU 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-H 0:;<9KOP[fghijklmt7uwxyz{}  Ofghijkl) 0;Omwxyz{}~;fghijkl) 0;Omwxyz{}~k& \ !0:;<=sKOPZ[befghijkllmpqtuvwyz{|}1+&6OO;<=  = ==)=Cfghijkpqvyz{== fghijkpqvyz{ 2 $0;Omwxyz{}# =fghijklpqvyz{< 0Omwxyz{}2 =Ofghijklpqvyz{&  OP > y=lOPfghijklpqvyz{ yy    =fghijklpqvyz{5 0Omwxyz{};n l0OPmwxyz{}nn  =fghijklpqvwxyz{}9 {=3Obfghijklcpqvyz{{{ =fghijklpqvyz{Olmwxyz{} =lS 0=KOPfghijklympqvwxyz{} O 0=KOPfghijklmpqvwxyz{} G 0=KOPfghijklmpqvwxyz{}A7 0Olmwxyz{} M 0=KOPfghijkmpqvwxyz{},OPI 0:;<9KOP[fghijklmt7uwxyz{}  %f,g,h,i,j,k,p,q,v,y,z,{,,,,,%% -o0O   =O  /  s;o<=KOP^?`>eNOO $ =%  = =OP O- =IKOP- =IKOP1IKOP, =IKOP%O"fghijkpqvyz{2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}2 0;Omwxyz{}&  OP < 0Omwxyz{}&  OP &  OP = 0;OPmwxyz{} = "0;OPmwxyz{} 111  1"0;Omwxyz{}  =fghijklpqvwxyz{}  =fghijklpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}D 0;=Ofghijklmpqvwxyz{}? =IOPfghijklpqvwxyz{}10;Omwxyz{}10;Omwxyz{}M 0=KOPfghijkmpqvwxyz{}       O  ))),,,555% =I% =I% =I% =I% =I$ =, =IKOP- =IKOP@  OPfghijklpqvwxyz{} %  I30;=Olmwxyz{}M 0=KOPfghijkmpqvwxyz{}=0;OPmwxyz{}'IKO/  s;o<=KOP^?`>eN;fghijklpqvyz{;fghijklpqvyz{U 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-U 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-(0;Omwxyz{}~U 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-U 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-(0;Omwxyz{}~) 0;Omwxyz{}~fghijkpqvyz{;fghijkl,   [;o<KOP^?`>eN0   [;o<=KOP^?`>eNU 0;"KOP[efghijkl/mpqt%u vwxyz{}2+-.  "&).35 6 8:<?ABCDELMNQRSTUVWX ){<.fiRhl$.Hz@,J 4 ( 2 H ^  X d l 8 Fh2X$B:x`,^TvF0~.0379ABCEMwxyz{} <"&.34568ABCDH5MRSTUW, )\ "&).356789?ABCDEGHLMNQRSTUVWX_dx&&O& )35678  )7<?BCEOl3HU )l )3578HCV  )3578<V 1H8 % ,<l,% <1"&.3568ABCDEH)MRSTUWmwxyz{}RUW <DUVWH^ <  <Hwl "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_ "&.3568<@ABCDMQRSTUW_?ABCEMSUVWX  "&.2?ABCDMRSTUVWX?ABCMSUVWX  ?ABCMSUVWX  "&.3568?@ABCDMQRSTUVWX_  "&.3568?@ABCDMQRSTUVWX_   < 8<8111 <  "&.3568@ABCDMQRSTUW_  ).035678<AV  ).035678<AV!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_!  "&).35678<@ABCDMQRSTUVW_  ).035678<?ABCEFNQRSUVWX"&.3568@ABCDMQRSTUW_"&.3568@ABCDMQRSTUW_$ "&).2?ABCDEGHLMNQRSTUVWXl < <DRVW <DRVW <DRVW <DVW <DRVWDRVW <DRVWX <ABCMRUW <C <CRUV <CRUVW <CRUVW < <FI)))FIVW,,, <F I VW555 <VW <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DRTUVWX <?DERUVW < < <VW <VW?ABCEMNQRSVX <?ABCEMNQRSVWX ).035678?ABCEMSUVWX  <?DVWX"&).3568@ABCDMQRSTUVW_VW$ "&).2?ABCDEGHLMNQRSTUVWXl"&.3568?@ABCDEMNQRSTUVW_ <?ABCEMNRSUVW )2356789<DRTUVWXd )2356789<DRTUVWXd  "&).35 6 8:<?ABCDELMNQRSTUVWX  "&).35 6 8:<?ABCDELMNQRSTUVWX"&.34568ABCDHgMRSTUW"&.34568ABCDMRSTUW358 )35678<DRUVWXl < r@ABCDEFIMNSU VW r@ABCDEFIMNSU  < < < <||40 yynnn{{   rl?   b  9f{}0678+.3$5=*?I3KX>Z\L^^O``Pd}Qk      00:=FFKKOOZ[eqs{&}}/0JQX]abdg  &)+./135:<<?ACF!HI%KN'PX+__4dd5fq6w{B}}GHIU[bjrsuvx)23+*+11 23 !"#$%&'/0 """"&& '**,-.,-.+/0*().&& /%.!#*+,*''(')  "$////-!!!!!!!++++' + )%"-$ @ latn  aalt>dligFhistLligaRss01Xss02^ss03dss04jss05pss06v   (08@HPX`hHFPb`^\Z^|Y|Xp GJ\PVPP&YH&QRDEXDEQXslixmpp-1.2.2/docs/_static/fonts/OFLGoudyStMTT-Italic.ttf0000644000175000001440000047274412473141653024201 0ustar mathieuiusers00000000000000FFTMU#ۂuGDEF]d4GPOSGSUB<L OS/2XVcmap5 cvt = !(Ffpgm/ $eglyfAJEwDhead |_6hheaT$hmtxw#loca:*pmaxp*x nameDpost^PprepBt_<nn"^"Zq^d 1oBPfEd  !Z2MD$;;B'$ ? ;*'1 5 u * *F*Be5,d'wI'EM$/'@''};, K 7 M ,  .. `M+    4N |D N;z,Q_'}G%G '-hGI#_'jIjIjb46'wwwwEEEE$/'/'/'/'/'O/';,;,;,;,)                          ' ' ' ' w w w w w ''''EEEEEM.M.M.T.H$$$$/' /' /' ' }`}`}`MMMM+++;, ;, ;, ;, ;, ;,  KKKh.M+rQyMd''I$$II$$I?D;;1'f q' ''''C>H<HCzHFGGGIG%GGG.GG?GG ""GGGIG%GGG.GG?GG ""GGIG%GGG.GG?GG GGIG%GGG.GG?GG ''''LI*?!;!__''~m: ~7I7 '  " & / : > D ` " 9L7 &  & / 9 > D _ "( sQPOJH8`^[SJGB(x   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a~rdeixpkvjsgwl|cn?m}bstz{vw7yx|HOqKLMzPNI,KPXJvY#?+X=YKPX}Y ԰.-, ڰ +-,KRXE#Y!-,i @PX!@Y-,+X!#!zXYKRXXY#!+XFvYXYYY-, \Z-,"PX \\Y-,$PX@\\Y-, 9/- , }+XY %I# &JPXea PX8!!Ya RX8!!YY- ,+X!!Y- , Ұ +- , /+\X G#Faj X db8!!Y!Y- , 9/ GFa# #JPX#RX@8!Y#PX@e8!YY-,+X=!! ֊KRX #I UX8!!Y!!YY-,# /+\X# XKS!YX&I## I#a8!!!!Y!!!!!Y-, ڰ+-, Ұ+-, /+\X G#Faj G#F#aj` X db8!!Y!!Y-, %Jd# PX<Y-,@@BBKcKc UX RX#b #Bb #BY @RX CcB CcB ce!Y!!Y-,Cc#Cc#-KPXYF+X!YKRX!Y EW+D E+D E+D E+D E k+D+\X E+D E+D E +Fv+D E $+Fv+DY+NB,DDUK)!@2G:BP#&N.64I8=zdl & v @ b rz Bjf,f !"@#0#%$%&p'"')*+,--../ /*/012*23R4D525>5J6789\9:;|;<==>\?8?@tA ABBCCCE(FFGdII^JJJTKKLLJLRMbMlMN.N6N>NHOPVPPPPQJQZQjQzRRRR(R4R@RLT\ThTtTTTTTTTUUUV VV"V.W0XxXXXXXY[\[h[t[[[[\\\\\\\\\\]]^ ^^"^.^:^F______aXadapa|aabccccccccccdde~eeeeeeghhh h,h8hDhPh\hhhthhhhjlDlPl\lhltllmnn$n0nnnnnnnnnoooo&o2o>pr(r4r@rLrXrdrpr|rrrrrrsttttttu uu"u.u:uFuRu^ujuvuuuvwwwwxxxx(x4x@xLyDz4^>Ԕ@,( ¢ʢ֢*6BNإ&ȦPابrhzRt$&bܲƳгڳ  *4>HR\fpzʴԴ޴$."*2:BJ¸4 .DZz2&P+ ++N 2+6 + +Q/ִ4 + %+' +/ +48+ +K B+K/R+' 994 )9998$9/,2@BN$9KE901!%27>54'.#"326323254&54>7>54&#"2    ,'4)1)     N0%@^5ː      +-MIVA  $  4C-D/+ +0/ֲ222% &2+%+ +/+ +++ + +/ +1+6?Hn+ .&.)&+>+ )')&+()&+')& #9(9)&'(.......)'(...@99.99 + 9%#9017467>32#"'.747<>32#"D  A  $   2    ! VA~u$Q{;&ewd/ET33 >h22d +@dM +\2/7s33 $222 +@ ++2x/^ְ_2[ +[+ + 2"+ +/" +!2f"+ +/ 3f +f +@ +22a `3  3O+P2K +RKO+Q3K)+- +.20-)+/3y+6?)+ _.Y  ?`+ P.'J.__+ _+?a+ _+ _+_+Y!Y +J/J.+?f<+ :J.+HJ.+PQP'+_`_+?_+ PkP'+lP'+mP'+nP'+oP'+pP'+YvY + _ #9vY 9kP'9l9m9n9o9p9HJ.9:9@':HJYkv !./PQ_`lmnop..........................@':HJYkv lmnop..............@a^c99[\9 99"9ROM9)K>Fq999089-+2990170546;25>54+"&54;256767632;2767632;2+";2+"#"54654+""54654+"7;27>54+"; R _ f bEQ_b  B$F_ b D/ dC    #3D  /. `Zj|X+E3 9+m2X +@XP ++ /f 9+62f' "+}/ִ[ %+[+ Q+/ Q+[S+N +NH V +V/H +|2HN+2<+;2+ +d  r+B ~+6?+ |.8 b b+ b+ b+?+ |9|8+:|8+;|8+?+ z|8+{|8+ b #99{|89z9:999@ 8bz9:;{|............@ 8bz9:{.........@[Y9S9V X99NP9dE`fm$9<x9 #699r'(?999B2+99f @ +/B[`dkrx$9'(9#901746323267>54.54672>323#".'.#"#"#"&54654#.327654#"32>54.#" 7 !..!tK  (7  8  '77'{d   >eo9  #/g *5%!#[ B( %- ;N !<&X\%% "  # ->`' %K4\n(  c D, % 3"F.0 %;;GS_Q/W 9+9/? "+]/K 9+,/ "+E/ "+E +@ +`/ֱ< <B+1 421H+T TZ+N a+<9B 9$919H/9T,99Z *KQ&$9N99WQ99 HNTZ$9,K4?632#"&5476?>54#"#"."#"&732654&#"4632#"&732654&#";sT 1,.%/ ޑ 5k*%9 6X#KH 0:8?nX66nK;>G -@8?XQ#"%  O( + M:9H-PAO7>M(4qUD7WQF7@K(9u'/}+9 + 4+k+] "+]k +@]e + +@ +1+$ L+$'  "+U+ME} +M "+EM +EH +,} + 9+/ֱ5 5 +" +  %+"P+B %+BP +BJ +*BP+ %+/* %+B+  + +<+h2x %+` %+`< +`c +xZ+n ++ 59"/199$9,}'999P99*9B9<?Us999`Vu99Zx]k99,E554.54>32#"54654&#"32632#".#"32654&#"32632#"&54>7>54&#"#"&54632#"&32654&#"';& +CE %A #MR 0(,*&* %#3pL^0*#2 !',7)NON+#$//H),@.AA.C;}Wy""7k   4R.$F m= (6&'M*8hJ|O*8+% @, #L4.)$+;D\O0$ *`B;5wA !$w?*W'/ִ + + + ++0174>32#".?1GM8 )>A8 +3+ $-(_f3 )E`c0"XQ= (Nc*!/ִ + + ++014>7654.54632#"7)>A8 +3+ $-(1GM8  )E`c0"XR= (McE_g3;ufA#*/?3B/7ְ22 /222C+014654&546323254&546322632#"&#"#"54654#"#"&;ll N   Q mm N Q  02 5 7   . 6 20 5 ) 4 5,O"+*/3 2* +@ +-/%ְ2 +2% +@ +% +@% +.+0174>;2=4>2;2+"#"&=4&+"&           *Zv1/ +/ִ Q+ + + ++014>7654.54632#"* %- &Y )2 9<:2t' /33 +22/+ ++6 + .=+ ++Ð+ +++ #999.....@ .........@017467>32#"'$8  ,     1q)+ +/ִ + ++01747632#".'.1!    .   Ir+/+6<+   +  + #9 9  ......  ......@01477>32#"1  77  $   P + f++ d+/ִ C++ 4++ 99990174632#"&732654&#" rTv-KZ/Sr;QLtfqY=d>"e^FLp^IQr!43+.3 9+')22+3 }+25/ ִ% +"$22% +@%, +@% + % +@ +@  +6+6?J}+ $.   + +  +$!$ +"$ +?jZ+ #$ + #99 9#$ 9!9@ " !#$.........  !#.......@% 0993,9017432>7654'"&5432232632#"2#"&"#" 8  < "&%F.D  "&:-  ^%   84+! 2+!4 +!+ ++ 9+  +  +9/ֱ  +  + + -2 +@ +:+ 4$9)+/3$9!499 990174>7654&#"#"54>32;2>7632+"#"+Z)J\D(L0@`N\N-B   A[ 5"wE)+. ( # #(66$ZB:  !)Gh@p>+ > + +#+ P+># + "+>#+A/ ֱ9  ' B+ 999 9'6999 90143232654&#"#"54>7654&#"#"54632#"&2HoN. $ 0r>/ 7g/8. !1:1*JzF,tq4-A   @C$0&,"$   4'!OK3 C>WT!+:3 M+B2! + +6+>+2+ +X/ֱ? ? +@? +?7+62( 4+Y+6?)+ 5P..+.+.+.+-.+5K5P+L5P+M5P+N5P+O5P+K5P #9L9M9N9O9-.99999@ O-.5KLMN.............@ O-.5KLMN.............@?=974:G999(*2S999!2(99 S90174>763232>32#".#"#"&4654&#"#"7326;2>7654#"  /      !  )@e^( 8 .  $2 J58:! '  #  G   -ECQ@+ +3/ +/ +" +D/ ִ< Q+ < +@  +E+/<$90143232654&#".54?>32327>32+*&#"#"'&#-?udg  4'f   *9J%^U sqYE[ LR      !+Ao\S  'j+ 9++% P+ + / "+(/ֱ "+ )+" $999%$90174>3232>32#"732654&#" M4L!<%_e{hG_>7654#"+"#"#"547>;2;2632+", NP    ?@ T25#3 3  tL!*!2@+% 9+./8 L+>/ "+A/ִ" C+" +3 %+3)+ 2; %+;/B+)3@ %.8>$9.%998$9> 990174>4.54632#"&7327654'.#"32654&#"*#22# !l_Mn()))jZo;JH95EWN H*42A1VJB=V+I+$4?rLI$: (?%a{`W=\$/ZL1+?E":T:-OB 3)h+ ++( 9+ / 9+*/ֱ %+ ++ 999%999 9(990174>32#"&5432>54"#"&632>54&#" 3nFWyN@n6pTrmq]5 KQcҔy56U}F']++" +(/ִ +   +/ +)+99 "$9"9017467>32#"'.4632#".'&F    "<,= 2   B53 3 BZ#V+$/ִ C+ + + ++ +/ +%+!999014>7654.54632#"4632#".'&B &- &Y"@.<  *1 9<:2t;: 5 )}74>767>32#"%.җ$ **6 k L8  op ZJ&b)}747%654'%&5432#"& 6**0 k A po 8L et I+E/$ E$ +E< +EH +J/>ִ7 +77+ +/ +7B+' K+>!H$97 <$92E999B$1@999'.9E 90174>?>32".'.4632#"54>54&#"#"e     ,@%0N     AB4'2     2-C4  $  A$tl-, EU+1 g+C/7 P+7C +7; + /+ +I  6+S/ 9+S +@" +V/ִ4 Q+4+F F.+ +W+.F@  17=CP$9S+@ &.4FP$991$9014>32#"."#"&54>323263232654&#"32>32#"&7326767654&#",A_xa*[ &$;/.?n:3 )Lbtѱ?f4 ,9Q+#6 (,1k`W9m$5,9] (ax! ;sl{ɡ! i"/Qa +&JP333 9+*C222+_ +^28W& +8 b/ִA +A +AE +c+6%+ ^..+^/^.+0^.+1^.+]^.+ #9]^.9/919091./0]........@ 1./0]^.........@A9 (998A99_WR9901747> 7>2#"&#"#"54>54.'&'&#"#"&#""#".#""3:632>54#" $8?]3  w '8 N&L2*""& .(!]UW   8@gqY',M,   ?H 1U' : 8<SfP5+8;33@ 9+2 +a P++ "+WH; +W "+g/ ְ 2T C+ T +@ +@  +TC+\2/ #2h+6?/+  . KS  + +SMSK+OSK+PSK+QSK+RSK+ #9 9RSK9Q9P9O9M9@ KMOP QRS...........@ KMOP QRS..........@T =9C +5@)Wa$9@59H+/I$9W)9a #99017432>?654'.5463232632632#"&#"#"732654&'&+"32>54'.#"   * 0PJYr*)**K40P#] K2HbcQTF  ,*\'<,??   NNQ!<' <)!JIQn9?[= "C F  0.:%Ro';U7+% P+%7 +%+ ++ g+322632#".'.'&#"327>32#".'0]`=t!    [aZ?p}[   zTQ)8JtE&+<+ !>eVg +" L8_v(A +- 9+&+ 9++3; 2B/5ֱ C+6?s+ A>?A>+@A>+@A> #9?9>?@A......>?@A......@ 9;- 990174>767654&'&54322632#"&#"#"&7;267>54.#"D.;2Y*NS^R/#= X,w)(9 (>iC>$ $  {~x#O9 A*%%a^@ =C1 'imh+\ h+ "+h +@hf +h +@a ++0 }++3 "+0 +@0$ +8Sf +8 S8 +@SH +8S +@8? +n/ ִ5 %+ 5 +@ +@  +5J+F +>FJ+A +F&+" +o+6?+  X 3 + +  +XUX3+VX3+WX3+ #99 9WX39V9U9@  3UVWX..........@  3UVWX..........@5 Y99J08S\$9F-f99AC9&a9"$c999S\Ec998C90 A999999017476767467654.543232632#"&4&'.#"&#"3267>32#"54'&#*&"#"32>32#"&#"#"$ $ AqB  2&V, =   N% &0NUw+ T H<# #Pb /  # $   0   K 8 0 w\ %j ;fe+^3 9++3. P++2+3 9+. +@.$ +8Qe +8 Q8 +@QG +8Q +@8@ +g/IִE +>EI+B +E'+" +h+6?'+  V1 + +  +  +  +V3V1+SV1+TV1+UV1+ #99 9 9 9UV19T9S939@  13S TUV.............@  13S TUV.............@>IG9BE@9"'9e\9QEZ99.8B99999017467>767>54.543232632#".'&'&+";27>32#"=4.#"#"&#""   " IyB I8")=J   663# "("B"; 7+, Xp 5$  '  ^*4 oR 'WU+* 9++! P+48U +?34 9+D2X/ֱ' '/+I Q+I/ +@IA +/I +@/6 +/I+ +Y+/'!8U$9I;O99 99D994*HO998'A999! 99014>763232632#".'.#"32>=4&+"543232>32#"&'0aBC?,U:-  &Q#BI5#}.>:& ! B ( *(6g%WdW"<)9<,EvL+[F4  7   +dj333 "+`m222+!J33 "+%M22J+D3 9+4|d +4 /vִU %+T2Uv +@UL +@Ub +vU +@vl ++6?"+  )?e+ T.Qs;  + + + + + + + +*)++)+s8s;+9s;+:s;+TRTQ+STQ+sts;+)+  #9 9 9 99999)9+9*9ts;98999:9STQ9R9@+8Qst )*9:;RST.........................@+8Qst )*9:;RS........................@Uv7>7>54.'&543232632323267>54'.54323263223#"&#"#"47>?654&#"+"3#"&#"#"   "3 < ?  2s  ( B7(   !>6    B: 7 "   .(C8R  0w-\K0  \pi     4  2<;+5833 "+4+/ "++3 "+2=/ִ +>+6?{+  $! +  +$"$!+#$!+ #9 9#$!9"9! "#$........! "#$........@199/;9,9+990174>7676654.'&543232632#"&#"#"  %"3 < ?    I4 #   ^qk++) M+) + ++3 9+,/-+6?~+   $!  + +$"$!+#$!+ #9 9"$!9#9 !$ "#........ !$ "#........@901463232674654'&43232632#"&  '+*D 4"!> ! l.' Bxf'   &"hE<;L|P+Xu{333L "+q22+ "+26+0339 9+&`P +& 9+'2}/ְ2$ %+#2$ +@$ +$ +@ +@ +~+6?=+ .l%3+ '.)b>գ+ X.^IF?|+ +l l+!l+"l+#l+&m+ '(')+b?b>+@b>+Ab>+Bb>+Cb>+ ՠ+ FGFI+HFI+^Y^X+Z^X+\^X+?5+ lil+jl+kl+ #9jl9k9i9"9!9 9(') #9Bb>9C9A9@9?9GFI #9H9\^X9Y9Z9@ )>?FZ\^bl!"#(@ABCGHIYijk.............................@ )>?FZ\^bl!"'(@ABCGHIXYijk.............................@$enx$9LP99&+996  .990174>764&'&5432326323267654&543232632#"&#*#"&'&'&'&#"#"&#"#" 6''5Q50K9 B/  :/B.#N  I=+J *(O +0   F  ?*  '"  ?5; !WI3W   6 H?;+7>33) ;+ 9+; +@0 ++3 9+@/A+6?q+ %$+ + + + + + #9 9 9 9 9 9 $ ........ $ ........@)"2990174>7>7654'"&54323263232>32+"&#"#"  = 4"8G%HCe5$  - "//J   59c505   "K  {r+3Y+_333U "+2_+\3d "+ +:3 9+?2/hֱH P2Hh +@H< +hH +@ha ++6>+  }{  + + +}|}{+  #999|}{9 {|}........ {|}........@Hh26\999U_Wap$9dS999@ '+3@Phjotx~$9017<>7>767>54&54'.+"54;263232>7>;2632#"&#"#"54>764#"#"&.#"#"&#"#" ! G*(&`A :   ,   F 3  = 6+ F-  AS d3* *d^׈2q4  + .@Na 5,*"   2s87<# /( " E" v6R+t3u+o3 "+o+r3+>3 9+A+;3w/ִe +d2e +em +x+6?M+ d.b  + + + + + + +dcdb+  #9 9 9 9999cdb9@ b cd............@ b c...........@e"999om99,14MNU`j$9+7H9990174>7>7>54.'.5432326323254.543232632#".'.'&#"#"&#"#"      .C p;9'  *$ %G) ;   *' *5)1"Ne32#"&732>54'.#"'5b[UP*/[^VpWA[GtF&:JtF9`xCGuGn[X5KZAftMF+C "+L+ "++0 "++30 9+ 2'F + "+N/*ֱ * +*! +O+6?A+  ?9 +?:?9+;?9+?9+ #9>?99=9;9<9:9@ 9=:;<>?.........@ 9=:;<>?.........@CLD99'!90 *9990174763676754'"543232632#"&5432232654.'&#""#"&#"#" &EETdD;'C) 9l&7    ..BF .(04 UQ@s jV!4 X`ID '8AQ3+* +*3 +*. ++O "+E3+R/ ְ2B ; +; +@;> +BJ+ 02S+; 9B@999J"*38&$9.9*3@9E"6;>$9O 8$9014>54.54>32332>32#".#"#"32>54'.#"'9W\%  "Gz]#'Q,9{. )UEoC$&xK~(%5"+R0/eu[=   `35 LJYJ8!1 ؂DoCfA3QiObe-+) 9+D2N+H3 P+K++3\ 9+2R:- +R P+c/Yֱ Y +@+ +d+6?+  @_ + +  +  +  +@=@_+>@_+?@_+`@_+a@_+b@_+ #99 9 9 9?@_9=9>9a9b9`9@ =>_ ?@`ab...............@ =>_ ?@`ab...............@Y-9)K+F999:2A99R$"99\90174>7>767>54'&546323263232#"&'.#"#"&#"#"&#"#";27>54&#"     >  : a+.DPQN g#/F;.5 ( !"8KY".KD-, -|x[bC1 V;:g"0 %C<3"6  <;4N&,_:HD+ P+++1 9+I/ִ ++6 6 +? ,? +' +J+9 61:D$9,-99?*99'# 991 *?$901743232>54&/&54>763232632#".'&#"#"'.YH*= "%u_(>#k   1F(',X!1bCj& 4H&!":%taP'8 #B .0#E.R"'C'69$ZT]+K M+2K +K +2/83. "+B2U/ִ +V+.205:999KS999014>7>3232?2#".'.+"#"&#"#"54>3676754&#"#"qfYZ /^-   K=   E-`:  = +  "H  3 [}     ,'G:+ +$)333+ "+!.F222H/@ֱ @ +@@ +@C  ++, +, +" +I+C@9 9:99,29 2@99",990143032632327654.54323262#"&'.5454.,##, : %P:A3+,( I !'>:aWf"%! 4.19#8F3 05=#"#FA D[5+ ++ "+&2+3C 9++E/F+5:9C,999 9 9901432326323254.5463232632#"'&'&'.'.$ 7 9 $   V"  Lky$2(+/ֱb c2+6N+ d.7+++++7c7d+ #99999@ 7c.........@ 7d.........@b.059$9e9$V$9(w9014323263232>54/.'.54323263232654.543232632"3254.543232632#"./&#"#"&'&''&'&'.> :   CA+     4 : - ["!6%      LH 5"  7>7654.'.'&#""&54>322?654.54323263232632#".'&'&#"2#"&#"#" .&8 H  A %+ 6#.)AK' AH*7%7[4!U$2 >$9? !!]4< ` <  ,!G[=Gc4 % D*%6 DQ'JW_  3uB) ?! */JO6!|G \<+C3$+$3( 9+Z22]/Oִ2 %+2O +@29 +O2 +@OE +2+& +& + +^+6=o+ WR++WSWR+TWR+UWR+VWR+ #99UWR9V9T9S9@ WRSTUV..........@ WRSTUV..........@2O@9,=99&)9(< )99014323263232>54&&543232632#"&#"#"54>7>7654.'&'.!N E2,+\\' >!   Z1   )2& )L^9|   *4   -*O&b 7WS+PV336 +:2&+ +)+ +2& + +>CP& +> +X/ִ + + +<+F +3<99CF99 9)090174>754#"#"&#"#"&54767>3232632;23254.54632#"&#"#"$e!P :#(1   +A<: M0+8uK;  ^w: .  5 1&)  #A074=2B+ 0/' 3/ֲ222% %+$2% +%, +% +4+6?+ . $ + +?+  + + + + +  +  +$$+!$+"$+#$+ #99999 9 9#$9"9!99@ ! "#$.................@  ! "#.............@'0,9901465767>7>;26232#"+" 32632+"&77 (  H) GS =Hj,M* -9/6 I +/+01432#"&'& 3 !    ?$4+=+ +*/ 2,/ְ 2 %+222 + + +-+6?+  .% + +  +  +  +%%+?+ %+%+ %+!%+"%+#%+$%+ #99 9 9 9#%9$9"9!99 99@ !%  "#$................@ !%  "#$..............@01430;26767654+"543632+"3 a) T^ 7 (= ,:/6k  =H&j,M* A% 1.!/ L+! +@! +/22/ִ +3+014767>32+".'."+" *)   K%G!!E&      "  %+ +/ + ++014>;2+"&  &  Q -Aa++1 7+"+ +" + ++; "+; +@ ++B/ֱ. C+;%'.8$901747676323263232632#"."#"&73267>54&#" 87>7654#"".54>7>3232>32#"&732654&#",  @*P359n1IBMBt9)%4 GD5W*)H +./J5m+5DX*< /(#_: #T"+ 4+" + ++ }+ + +$/ֱ  + +%+90174632#".#"32>32#" g *0 %' v$;! *Z2k # #N) --=LK;+13A 4+;+( +(; +@(, ++ + "++I "++ I+M/ֱ> >F+E2& +& +2 B+  +  +N+6?d1+ .D $DD+?Y+ $!$ +"$ +#$ +DED+"$ #9#9!9$ !"#DE........$ !"#D.......@F>4;999&199 (9I(.68>$9901746323254&#"+"54>7>23232>32#".#"#"&7327>54&#"[ /" %    # U+#>"=;F*+<)+,R0}s    =Uo+ c#*##*#IX/84 2Tv s,v+ 2+ + ++( 9+" +" "+-/ֱ  + +%+ .+% 9999("9017467632"32>32#"&732654&#" <58S$3$ :cHH1 s;YER Gl#%-!?|15* -):C HTP0/JLJH+ H + +!+) L+)! +)& ++.3 52K/ֱ+? C+? +@?3 +? +@ +L+H99?99?9399)901463232>754+"&546332767632#"&#"32632#"&#"#"&+  ( = 2?9E/ 2!B 93 8% 70/G   ZP%+)  >w!+4 =>MY<+B "++! 5++W "+$!+ "+Q-< +Q "+Z/ִ? 4+?+N Q+4N+ +/4 +4 +@ +NT+* C+D 9 [+K99N0GH999T54.54>54.5463232632#"&#"#"&#"#"&73254&'./"32654&#"SBJ 2 $ Q>.P_P[SVw7[9pGi  &U'#7)"5I&  :05k   $8t  &5)<0=I58A)+ 079OA9>QD%2+C3' +'2 +'- ++ + "+ +9 L+E/7ֱ" 4 % %4 +%/ +F+6?J|+ @+++++@=@+>@+?@+ #99999>@9?9=9@ @=>?............@ @=>?............@9'"4$9 90174>76654#"#"547>3232>32327>32#"54654#"#" &  &/6 <!7Y    `"19"C ! AV  6 q."T]N(/I &8&DXE+W3= +=E +=@ ++ + "+-++P&E- +P 9+Y/ְ2T RS22T0+Z+6?[+ .U.+ IG49?Q+ +++++?Q+ +UU+U+U+ U+!U+=+ 4549+649+749+849+IHIG+URU+SU+TU+ #99999 U9!9999549 #9697989HIG9@45TU !6789GHIRS..........................@45U !6789GHI.......................@T 9990 =E$9&PN9-J9 90174>?>7654#"&#"543>323267>3232632#".#"#"  /C   zMM2  . F1'' ! @Ya(-  *:E `$ 3D/Z 8PESE  .3-/+# +#/ +#( +++ "++4/ֱ22 + * +  + 5+6?a+ .++++++++ #99999999@ ............@ ...........@  99 9# 9990174>5>7654#"#"547>3232>32#"..  (F     25  VVN"  UWsU:*f3+EZ33% +%3 +%. ++33; O2+b +b +@be +g/\ֱW WG+C C9+ # 6 6/# #6 +#0 +h+W\_b999G O999CJ996;99999%36CGV\$9b@  89J_$9;99901463232>763232>7632326?>32#"&5464&#"#"54654.#"#"54654&#"#"E +; @<%   b"#8 '$  $! #- " BV '  J  >* #   K%H)9/ P 8 H3- "+:G!+:3 +! + + +* O++C +C +@CF +H/=ֱ7 7'+  $ $/ $ + +I+7=@C999$*999' 9!$5=999C&'@$9*90146322>7632326?>32#"&5464.#"#"&54654&#"#"F I1T   c!#"    ,% AT54  9}( $  P%+%,A/;  +8  J + 9++ 9+/ֱ + +  99990174632#"&72>54&#" kGK3lFBWM1L;1%&FfeLAxS\VHL6QQ"CDC:F<8Xmj>+A3] "+W+W+M "+/++-33f "++ P++/ / "+n/`ֱ1 o+6?iS+ Ik+++?R+ +++IDIk+FIk+GIk+HIk+lIk+ #99999HIk9G9F9D9l9@DFklGHI..............@DFklGHI...............@1`79>M9 ]17`999f990146767754"#"=47>2>7>763232>32#"&#"#"&#"+"732654.#"#")6 (    &'    ;I' *)E % Gr'4!   "5   , ZSB  *  #  I'9Mo87GU5+; "+& "+2+D "++H/ֱ8 I+5&*99D;+1/$9017467632326326#"&#"#"54>7>54#"#"&73267>54&#"D-Lb"' (  .:%  -L.#;D0&0e* 3c6t32RN &C&f!  *nB''AJ&8_If!b~V4r&++ ++- +- +- +@-3 +5/ִ0 +0(+# 6+#(+-$9-& 999 99014>3232>32#".#"+"54654#"#" $=!   ( +"  ( 4#B34  x5! *D%07r5+ 9++$ 9+$ +@$! +8/ִ& C+& +0 Q+9+&5999 $)$90!9$0$901743232>54.'.54632#"&#"#"&  $  M;])56 B-(G6$$  2?485$2 (-=u5+, +,5 +,0 +@, ++ 2+>/8ֱ( (8 +(2 +8( +@8 +?+(8;999,8;9999014>7>3232632#"&#"32>32#"&54654&+    ! >   ( h+&*;    =  fKH*46 F9++3 ++3@ +@ +@E +G/;ֱ > ;+ Q+> +> + -+ - + ( + -+ H+>;99  9-69 13999 9-9@@ "&(136;$9901463232>76763232>32#"54>54#"#"54654#"#"C.%    G-!=!^>  mS"-  &&`" -$   V %%c6H2 !@<# $3+ ++32 +4/ִ% +% +.2 Q+0  + %+ + +5+ %")2999 099 92  )$999017463232654&54632#"54.7326765654" W$5c !3@7A  2Gj0!;%X/ 6#(c*$1!! hM AO;+13 +2+)33M +P/ִB +BK+ C+ = Q+=/ Q++ 3  4+&++ Q+B@9=?FM999 K;993 7999919&9+#9M @ #357=F$9&+$90174632326767>3232>54&54632#"'4#"#"54.73267654#" V# !F  #J+ !-q$9#+8="!  *Ep2'4&\2?x 4.1^m40 -))VZ(89( -! &+#AO>+6 M+ +6> +@6 ++ ++2%+. + +@ +9M+P/ִ + +1 %+Q+ K9991 I9996>;99 "1DIK$9.(9017432326?4.'&#"#"5467>3232>32#"&#"32632#".'.#"#"&# ;   ! *#9 R83% -P*) M'&3A # -1<12<2m }-(   ;Z[Hz,++ 3++ + ++#3 +  +  +-/ִ ++& %+& +! +.+ +$9 999!&9901463232654#"#"54>3232654&5462#" ,'U$ '!K4( "+(\qS} !#1 %&71YjY|20 *eIG+4+B+#++ +  +  +;/2 "+2; +25 +J/ִ + + +&+K+&D999G20A99  (-D$9#'9901747654'.#"#".546323263232632#"./&#"#"& #;  0\ - #% (  <)  =*      #1/; ' ?'6# G1 4F//.+'3+3+ 3+0/1+6?G+   +  +  +  +++ #9 9 9 999@  ..........@  ..........@.+901454&54676763232632#"#".#"#"4+' $ &H)" % p DC ! z  N/!-"/!ִ %+2 %+22#+! 90145<&54632#"&5N   WKh,L+  'F)OtJa2&)F0/+'3+33+3 +1/ִ +2 + +2+6?S+ .'  + +  +  +''+#'+%'+&'+ #99 9 9%'9&9#99@  #% &...........@  #% &'...........@9 /+90147>5>4#"&543232632#"&#"#"#% % >% & /%  !  xn   l G_ D/ 6+ + ++ 7+ + +/+ ++0174>3232>32#"&"#"4G#:A( +.-),.D/+*/ +0/ְ2 2+ ++# +1+6?Hn+ .>+ +++ #999............@9-9  *99#(99* 90174>767>32#"47632#".'&D     $6  !VA{w{ w   HXE+O !+E+( d+E( +@E< +3+@++V P+V +@ ++Y/ֱI I>+?2: +: + +2 +@. +2Z+6?+ ?..?: ?+ ?? +? + ? +::+?+ ":+#:+6:+8:+?@? +?+ Q? +R? +Q? #9R999 98:969#9"9@ "#68:QR?.............@ "#68QR@...........@>IDE99:CO99 &4V$9 (999O(&9V ,.$9017467>2>?432#".'&#"32>32"##"54654&*&'&732754&#" \L 2  #!:" H+   eI# %KV $ ( R    %+ ;&5# <mbmZ+N +`+f "+NZ +NS +\NZ+ M+#+0 P+0# +0) +lZ / P+:BZ# +3: 2n/ִc +c +5 5 +@  + 5 +H H +@H= +5,+& &o+ c`fl$9 \^i$9H5:DL999,#0;BNZ$9f`^9N\ci999lL9B H99:90 9901746323>54'&'.#"#"54;254.54632#".'.#";2+"32>32#"'"#"&73267.#"##!  0 7}U;W"   1$) q9F,E  $B(Ng*# "$%& *-c !5nw6*$'  =+.b  0'R$ % /8(,  /8)/3 7+)3 +@)- +$27/ L+7 +@ +29/ִ1 4+1 +@ + 215+ C+5 +@" +2:+6-O+ +ұ+ & -O+ +ұ+ &+ +-O+ ++++ұ+  +'&+-O+ +*++ #99*+99 #99&9'9@ &'*+................@ &'*+................@0174?&547'&4632627632#"/"'#"&264&#"D'&C E45E E))E E43E hRrSS9:, E3DB3EE''E F8>=7FE'&DvQStUm+t32+,$36 9+'22t2 +U3 ]2t2 +H3 D22/ֱS S +@SF +@SZ +@Sj + c 鲀c +@ +@ +@v +S%+4 +%4 +%* ++6+ ++++++++++ #9999999999@..............@..............@cq9S`9%;B^hn$94:9Z96 7990143232632;:>7>54&&54323263232+"32632+"#"&#"#"54>7>7654&#"&#"54;267654654'&'&+"&546;254./&'.!N E2'+@E' >! 6L] V   ` ~    Z1   >3   _  D M2&  '$Q%  Zt   Kp  @    _  [    b N/ =!/ְ2 %+ %+2 + /"+ $90146465432#"&4632#"&N     *g p+"&[D &;KTkO+ )+#+7 P+l/ִV Q+ V+: V]+@ 4++2L@]+ 4+/L 4+@2 +2/m+ 9:V9@ #7=OZej$9]FH99L3&99@2)0997  .0LZj$9)&+9901743232654.54>54.5463232632#".'&#"#"&'.32654.#&#";!4!DY@[[@!!##qU,E  -7>=VV=wi*<' U-7  4  (2G0.#*I1#;# 1$Jn _ &)3F$ - 'H3#<% JpnV ;H&    zoX, N + I/7 P+7I +7= +// 9+/ +@/+ +/ O/ִ + +2 2 +2) +2;+? +?+ +P+;2 I$9/7 !$9014632#"&7 654&#"463232632#"'&#"327>32#".,ϕΗ*N}p&M- ;nK`)T9P8    '$&?_4 OӒϕf" k\K#CD*O :  "/?/H.?2+, g++  + +.+;/ "+@/ִ/ %+/9+ +9 +@ +A+9/,$99999;$/9$990146323263232632#".5#"&732>7>54#"dH &   6   '.* ,0'9  >K ' :   *)l @T8]*P/%3 +2+/ֱ +  + + +  + +" +,+01747632#"'&'&74762#"'&'&cK >"{ dJ +*"z ge?$z gh-,#(3/  + +/ ִ +  + ++01743!2#".=4#!".     o ' Q Xh + 9+4+DJ33, "+/@N222 / 9+9_4 +9 "+e4 +W3 "+i/ִ + Q+= +Y2=Q +@=B +Q= +@QL +=b+! +!+ +j+Q IW$9=G9b@ %8$D$9!&)99,14999,4-1BL$99 &8R$9_%$99e!U999014632#"&732654&#"74;2632;632#"'.'#"#"&#""54676=4654#"3232654&#"zVX||XVzjHJkkJHj@ ,%2    8$0  N ! "X{{XV{{VHiiHJjj& !  _  #87!}3R`U H+ / /ִ ++ ++ 9999014632#".732>54#";+02K,*!9&c#CG$/7&"=#I 5b / 3/%3 23 +@3+ +3 +@ +6/.ְ2) +2). +@) +!2.) +@. +27+0174>3!2#!"54;2=432;2+"#"&=4&+"&  V     Z        %^"[YR 9L=+/3 !+'2= +* +K+ +3M/ִE +E +EH +E+ C+ 1+$ $1 +$, +N+E=99 99189$4699=KE99A9 168$9014>767>3232>7>23232632#"54654#"#".#"#" '   %2"_H > & T5(2!F -^(U_2 $pA    H$ZHx#J$!,'6='G5Ka+& 9+& +&! ++F "+W2280 +8 "+b/ִ6 +6*+R+ +D +D/C3T+c+6?+ C.(Y](*(C+?u+ +(C+<(C+=(C+>(C+?(C+@(C+A(C+B(C+]Z]Y+[]Y+\]Y++(C #9<9=9>9?9@9A9B9\]Y9Z9[9@(*+@ABCZ[................@(+@ABZ[..............@6&999*08J999D-FLN$90&N999F899014>326326;2#"'&5463232657654&#".32>7>7>54#"32>754&#"'G],P%  $ tP% D /9&!  '8  ! Is5 3eev6 ;OL3B }N+W1. Xxx h ( / + + /ֱ  +014632#"&h% $ L,&'-9_I_ N+ "+/ "+/ִ %+ + %++  9999014632#"&7327654#"RI00PF-83 48#.?c>/?e95+,:*0O9:'TK]-P/+3 +#2./ֱ  + + ++( ( + + +/+0174654&54632#"&74654&54632#"&'dJ +  *34{cJ Zfhi- !#+, gj ]z ID'#'GIE'#'GD'#'G4C(D[&+ & + +! +E/ֱ + +F+9 &)*C$9.>9990174>7>54&543232632#"&4>762#".'4     BB5'@%0N   #  h! $  A$tl--B     'Q$'R$'S$'T$'X$'Y$'}j+^ g++k3 9+s2 +@h +^j +@^c ++ .122+ "+ +@% +{h +3{ 9Uh +9 }+U9 +@UJ +9U +@9A +/ִ + + +L+H +?HL+C +H(+" +(" +@( ++62wأ+   ?+ vZ42/+  + +?+ ZWZ4+XZ4+YZ4+vv+?I+ v+v+v+3;٤+ + #9 99v #999YZ49X9W9@ 4Wv XYZ.................@ 4Wv XYZ..................@9L19^kp$9?J.h999CHAE99(c9"e999^9{e99HL99UG99E9C999901747>?654&54;232632#"&4&'.#"&#"3267>32#"54'&#*&"#"32>32#"&#"#"5467>7&#"#"&#"#"&#"#"3:637>54#" $CMH'=l5  2%X,>    O% %/OUw+ U H=# % H$G. &:3 5+5.fH :  &0[h]2  " $   0     L 8 0 w\ $ j  G1U'  :0K'9'_X&i'Q[(i'R[(i'S[(i'X[(2'Q,>'R,4'S,T'X,9f*7+7+ 9+1+> 9+'+"3L 2U`7' + 3U 2g/Fֱ* h+6?+ fP+ ++fQfP+RfP+SfP+cfP+dfP+efP+ #9 99efP9d9c9S9R9Q9@  RScdPQef.............@  RScdPQef.............@19U`Z99L*F9990174>7>?4&+"54632326?654&'&54322632#"&#"#"&7;267>54.#"32632#"+" 2 4.;2Y*NS^R/#= X,w)(9 (>iC>$   CJ   }89   {~x#O9 A*%%a^@ =C1 ' t55 <={'T1''Q2''R2''S2''T2''X2O#.{'/ +' +@', +"2' +@ +2//ֱ  +@ +2 +@ + 20+6-Aҿ+ * ҿҿ+ $ -Aҿ+ +ҿҿ+ $+  +-Aҿ+ +**+ҿҿ+  +%$+-T+ *)*+ #99)*99  #99$9%9@ $%)*................@ $%)*................@0174?64/&54622?62"/&#"#"&Ox | t }  y| { w=y |s } y|| x'?Se{(+X P+(X +@(3 +0++O 9+O +@ + +f/ֱ@ @\+# g+67v+ 81 8 8+7v+ 11+1+1+1+01+7s + 898+:8+H8+I8+1b1+d1+98 #9:9H9I9d19b99999@189:HIbd..............@ 0189:HIbd................@@69\(33232?632#".#"#"&54?4.7327654.#"32>54'&#"'5b[-  # & ##/[^6% 0 3 ,-V , $?GtF&'F"WAPi:JtF> B 1;R.GuG SV*[ +! "+!> +@!$ +]/ֱ 2 +@ + +@ +@ +8+' 8' +8/ +^+6?{+ D+DD+?hQ+ AD+BD+CD+ #9CD9B9A9ABCD........ABCD.......@K998$>QU$9[K95,/9>'9 90174>754.'&54323263232632#"&543232654.'&#"#"&#"#"##"3 < ? HTcD;)A) 9l&7'     I4 #  ^TR@r jV!4 (XPCe1+; 9+d+ d + ++N 61+f/ֱF+# 4+#@+K2- Q+2@- +@4 +g+6?p+  _Z>x+ [Y=(#+ _ZZX?_ +  + + + + + + + + ++ZYZX+_Z[Z[Y+[Y_[_Z+?&+ \_Z+]_Z+^_Z+  #9 9 99999^_Z9]9\9@X\ YZ[]^_....................@X\ YZ[]^_....................@FV9@#!'DIN$96;4@99N-D9990146322>7>7>32#"&543232>54.'&54>54&#"#"+    E31.8.%+B-(H! $M-6-,      8%$ 7  20N$,7h*A+$+8+)L1;80  (-#$,P7 D3N*)4 #f? K+4 'QD 'RD 'SD 'TD 'XD 'YD :JW-+$ 2+8+> 7+$- +@$( ++H "++S 9+ +M8 +M "+X/ֱ; ;E+ C+K2E +@ +P+ Y+E;38999!99P $-$9(*99>-69$*35;$9HMEKP$90174>32326322>32"32>32#".#"#"&73267>54&#"%32654&#" E~E0 #9"$3+%:dHH1 s;.@&-G"@+F-!.E (F~:Gl#%- r:k1E * !9):C H##&/&EB$,?rZP0/J 9'_F s'QH 'RH 'SH 'XH'QZ'RZ'SZ'XZ AP#?+E 9+!++L 9+Q/ֱB BI+; ;I +@;0 +R+6+ -22+-+&-+'-+)-+*-+,-+32+- #9&9'9)9*9,92939@ )*,-23&'............@ )*,-23&'............@IB ?$9;9LE;999! %59990174632&'&#""&54654.'.54322>?6?2#"&72>54&#" k5% <9  ! #I  54.#"32>54." k 3kF     M  &F( &;f 4 -4AxS 00 4,C:F6QQ"''QX'RX'SX'XXHz'R\<g|M+P3l "+f+f+\ "+++#+' "+0+2+:+u "++++++ M:+}/ֳ$28 8 +@8/ +8o+@ ~+6?gC+ 0.X++++?d0+ +X1X0+2X0+?b#+ 3X0+4X0+5X0+6X0+SX0+UX0+VX0+WX0+zX0+{X0+ #99WX09V9U9S93919{9z9694959@6SUz{1345VWX..................@6SUz{012345VWX...................@8(P_h$9o+>M]lx$9@F9M\9 l@Fo999+'/9014674#"#"=467>465>54#"#"&547>3232>32#"&#"#"&#"+"732654.#"#")E( &  $7&'    ;I' *)E % Gr'4!       (wM   0~ SB  *  #  I'9MoHz'X\h'U$ `'UD{'V$ s'VD=]mb+V3 9+O220+( g+(0 +(+ ++k +j2Dc +D n/ִM +M +MQ +M2+% C+%2 +%- +%9+:2 +o+6+ j.:+j;j:+ 7>2#".#"32632#"547>54.'&'&#"#"&#""#".#""3:632>54#" $8?]3 w  @Z 8K;& N&L2*""& .(!]UW   8@gqY ":5M,  9$$ +B3-!>G 1U' :  =<O;+@ 7+0+*+" g+"* +"% ++J "+ +P/ֱ= =,+ C+, +' +Q+,=;@997J999;",/999@19J@ 357$90164>32326323263232632#"54>54&/"#"73267>54&#" I~A5 )/ H F, 9J% #+8@-!.E , F"!6xk 1 0 0&A *= %'$ +B/ .##$,?v\,%8Y''R& 'RF''S& 'SF''W& 'WF''[& '[F'[w'P'] Gap_+U3e 4+_+L +L_ +@LP +.+ #+' "+3++m "++ m' +93 A2m.+q/ֱb bj+i2J +J +7 7 +@7? +@72 +7 +@ +@% +r+6??3+ hH4hh+?Z+ h+h+h+?Y+ H5H4+DH4+FH4+GH4+hih+h #99GH49F9D959@ 45DFGHhi............@ 45DFGHh...........@jb X_$9J ()U$97*-BL$9mLRZ\b$9 ?9017463232654+"5432;267657654&#"+"54>7>23232632+"32>32#".#"#"&7327>54&#"[ / G,  %    % =$# U+#>"=;F*+<)+,R0}s r  /   S+ c#*##*#IX/84 2Tvih'U[( `'UHi{'V[( s'VHi'W[( s'WH=i}+\ 鲃+3 "+u+m g+mu +mp ++0 }++3 "+0 +@0$ +8S} +8 S8 +@SH +8S +@8? +/ ִ5 %+ 5 +@ +@  +5w+j C+jw +@jc +jr +jJ+F +>FJ+A +F&+" ++6?+  X 3 + +  +XUX3+VX3+WX3+ #99 9WX39V9U9@  3UVWX..........@  3UVWX..........@5 Y99w08NS\}~$9j9Jh{99F-9AC9&a9"$99}mhw99S\Eac9998C90 A999999017476767467654.543232632#"&4&'.#"&#"3267>32#"54'&#*&"#"32>3232632#"54>54'.#"#"$ $ AqB  2&V, =   N% &0NUw+ ' >$ 8K><# #Pb /  # $   0   K 8 0 w\ %A )'$ +B1  =s4A*+" g+"* +"% ++= 9+7* +7 "+B/ֱ ,+ C+, +@ +' +:+ C+,5799919:/0=$99",99=79017467632"32>3232632#"54>4.732654&#" <58S$3$ :cHH1 0?A 8K!./!R Gl#%-!?|15* -):C %#$3$ +B1  EP0/Ji'[[( '[H''S}*='SJ'{'V}*=s'VJ''W}*='WJ'9'^W*='\J'S}+'SKD+333 "+{222*+0T33& "+4W22T+N3$ 9+* + * +e33 <`22/"ֳ!$29 829" +@92 +"9 +@" +@" +9F+B2\ \F +@\c +@\} +F\ +@FL +p\F+ %+/p %+p +@ ++6?+ !.7!+!+ !+!+!+!+!+?+ !+ !+87+?a+ 7+˜7+! #99 999 9799@  !78................@   7...........@9"-$9@ 0?JN$9F99\pQky999}999n99m9kl99c9$9F\999&5X99T(2LV$90174>7>76764&+"54;2>465>54.'&54323263232632674>54'.543232632;2+"23#"&#"#"47>?654&#"+"3#"&#"#"32326?4&#"+"    / :"2 < ? 04 ( B7( . 0     !>6    B: 7 "#$ AD  >`    Q  0  BUR     4   J6eS+d3H +HS +HN +$++! "+'+A+Z L+6+Z! + 36 2f/ְ2) %+(2) +@)0 +@)& +'2:)+ +/: +: +@ +@ +)X+C U F FU +FP +g+6?M+ 'a++++??6+ +++a(a'+?E]+ 8a'+^a'+_a'+`a'+ #999999_a'9`9^989@8a(^_`...............@ 8a^_`............@:"9)<9U$+56>AZ$9FX.299ZH:<>CU$9+6 0999$!&90174>?4+"543232>7>54#"#"547>3232632#"+"32>32327>32#"54654#"#"  %+    &/6 0W9>!7Y    `"19"C ! 7v;4   t, q."T]N(/I d'T,"'TZ7h'U,`'UZJ{'V,s'VZ=2QP+23 "+D+< g+754.'&543232632#"&#"32632#"&54>54#"#"##"3 < ?    > 9%$  #  ^qk #.$ #' 5!=;G9+2 g+29 +25 +++ "+E/? +H/ִ/ C+/ +/6 +/+  + + + < +B '2I+/9 ,99 "?E999B%9299014>54&54654&#"&#"54;2632326323262#"&4632#"&" -   A %  !>% #9$'& $ !-$"(   N4 ( !" $ &&-&',2'W,$e+ + + +++# "+%/ֱ  +  + + + + &+# 99014;263232632"54654&#"&#" A %  7J-   N4 ( ;S( '-,8'ML'S-8'SID9'^-.9'^NH'R/.'Ru*O9H'^/.9'^>OH'] /.I'] OH'y'/.'yOC]Y+G 9+\+U3 9+\ +@N +#+*3 9+^/ִ4 %+4 +@4, +4 +@! +_+6I+ 8 >?+ C1 + + +8+ >+8+?ms+ +C2C1+I+ 78+?>+@>+?p+ CBC1+8 #979@>99?9  #9 9 99BC1929@ 1278?@B>.................@ 1278?@B>.................@4'DY999Y9G/6P$90174>7>7674574#"#"54?67654'"&5432326323?63232>32+"&#"#"  2 H = 4" 6  %HCe5$  - "//J  ,\8$,5   xxP  R"K  W1H+< +54#"#"547>3232>?632#32>32#".54>54#"#" > (F $ 3    25  = K-\  EH  F:*!w_'R1'RQ9'^919'^Q'[1'[Q$'Qw'h'U2 `'UR'{'V2 s'VR''Z2 'ZR'`},Z+N g+^+d P+ZN +@ZX +NZ +@NS ++ 3" }++y 9+" +@" +*E^ +* }+E* +@E: +*E +@*1 +~/ֱa at+' %+'<+8 +08<+3 +8+ ++ta[^999'GK99<"*EN$98X99359S9 U99Ndh9E8Uip$9*5a99"3t999y 9014>3232632#"&4&'.#"&#"3267>32#"54'&#*&"#"32>32#"&#"#"&732>7>7654.#"'5b[  3&V-=   N% %*TUw+ U H;N4Vp $*<0GtF&:JtF " $   /#    M :/ w\$ j   - ]/ Aft 3BO%+ 2+1+7 9+% +@ ++3> 9+K2E1 +E "+P/ֱ4 4;+C CH+ Q+;4199C +999H %$9 "99"+4$9>E ; CH$9017463232>32"32>32#"./#"&72>54&#"%32654&#" k1%H,$2+%:cHH1 s;$7#6AYM1L;1%&F0Gl#%- f$* !9):C H_VHL6QQ"CDC:F)P0/Ji'R"5V'RU9i'^59V'^MUi'["5V'[U'R6F'RV'S6<'SV9'_690'_aV'[6H'[V9'_79'_DW'[Q72']Wu$I+M "+>2+l M+2l +l +-7I +X3- _2v/ִ +w+6?{+ Rg:#%:#+&:#+':#+(:#+):#+RTRg+eRg+fRg+TRg #9e9f9(:#9)9'9&9%9@ #%:RTg&'()ef............@ #%:RTg&'()ef............@MIAK997V9-0]99lt999014>7>3232?2#".'.+";2#"&"#"#"&#"#"546676767654#"#"54326;2>7654&#"#"qfYZ /^-  :R!  K=  2n /\ -`:  = +  "H  I=+      D       Y=N+E +EN +EI ++( %2+XN +;3 42X +@ +Z/QֱA AQ +AK +QA +@Q +@Q + AQ+2 %+2 +@29 +[+6>I+ S? ,-?,+.?,+0?,+STS+TS #90?,9-9.90?,-.ST........0?,-.ST........@ QW999A992=99XEQ999( 299 "9901746;267654&54>7>3232632#"&#"32632+"32>32#"&54>54+"#;+    ! >   < `( h+&   O     =  -  V H*4<',''T8'TX,'h'U8`'UX,'{'V8s'VX,''Y8'YX,''Z8'ZX,='a9+ P+Q+K+C g+CK +CF ++$)333+ "+!.`222b/Zֱ Z +@Z +Z]  +M+@ C+@M +@H +@+, +, +" +c+]Z9M 999@ S99Q999,299CM9 2Z99",990143032632327654.54323262"32632#"54>54.'.5454.,##, : %P:A3+,( I !'>:?  9K,3:%! 4.19#8F3 05=#  $ #B- FA =^Q+ +A+;+4 g+4; +47 ++3X +X +X] +_/Sֱ V S+ Q+V +V + =+1 C+1= +18 +1E+" "E +@"* +"E+ `+VSQ9  91=@N99"E/BIK$9$99Q4/=99 E9X@ "$(*IKNS$9901463232>7>763232>323262#"54>54&54>54#"#"54654#"#"C.%    0!=!)! F, 9K  mS"-  &&`" -$   %`F8 %%; %'$ +B+H2 !@<# 'S: 'ShZ'SJ<Hz'S\'XJ<7'R'='R]7'W'='W]7'['='[]h?3K1+ 1 + ++ 9+ + +4/5+6?+  ,$>+  %#?Z+   +  +   +   +%$%#+%#,%,$+?|+ &,$+',$+(,$+),$+*,$++,$+  #9 9+,$9*9)9'9&9(9@ '( #$%&)*+,................@ '( #$%&)*+,................@0146323265>7>32#".'&#"#"& &W $W54J '/   #^.  EQoJ&.-  +:9=+/UiE?>LI9'^690'^aV9'^79'^DW82-+ .+ +++ "+3/ ְ 2 2  +  +4+6?s+  ."  +  +""+"+"+ "+!"+  #9 9!"9 9999@   !"...........@   !".........@.01999014323267>54+"5432632632#"&'.I (  /    *P7[  g=9J"    \K ]rOS[[esVW3YQ TyZK ' / + /ֱ  + +01462#"&* jz 4G  / +/+ +014>762#"! $ 9$ +/ + + +2/+ +0147>;2"&#"#" %Sa$  B$UT @/ M+ + ++ M+ + +/+ +01463232>32#"&#"#"PH OJS0 O03`( /  /+ + 990143232#"&#"MB%E s5/ L+ +@ + 2/ִ + + +014;232>;2#"&  ( * 5 /6`$- ?_ $ / + + /ֱ  014632#"&& $ !Q-'(, 1/3 +2 +/ֱ  + 014632#"&74632#"&#&$'M ' &" ' &"| H / 9+/ 9+/ִ + + +  9999014632#"&732654#"5 >&! $D"4-&-*$, /3 +2/ִ + 99014>762#"74>762#"" $ " $ 9$ 9$ !2/ + + + 2"/ִ +9014;2327>32+"'.S/    %U) $  B%U-/ +/ִ %+ +  +@ +014632#".?"1*3  ! KV/+ +/ִ + +@ + +01632#"54>54."> *3  9W/ + +/ִ %+ +@ + +01632#"54>54."?O! *4  9W^' '   / L+ L+/+015463!2#!"& @     / L+ L+ /+015463!2#!"& G    / L+ L+ /+015463!2#!"& S  tI3+ +/ִ C+ +  +  ++014>32#".I46  +  1W* ! 0&$3+ +/ִ C+ + + ++014>7654.4632"$  +  46 1&1W*$1/ +/ִ C+ + + ++014>7654.4632#"$  +  )& / 1&%G,I3+ +/ִ %+ + + ++014>32".I  0 P#: #;I+<;I #9=9>925'93949@ 5;..........<234=>......@5DF9G9LN99,(%/$99NR9 99999014>3232654&543232>32#'.#"+"57>54.54>74574#"#"? %F* ;6"M4 F,       (K+ "( )d@T%< /'5 )Of` EED)8K5;3 yiu+e3 9+X2ux+a3 +\2+1+(e1 +G3( "+92+C3$ +=2z/ ְ 2V +VVk+o +o/k + s h +V+K +KKO+ +/O +/+3 ++ 7 +I27+ +@73 +{+ou9sm99 9h9V/9(Xe$9KkQ97P9O19G999359S^99 KQ$9 99$(+?991/599014>32325654&4>74654#"##"54>3232654.543232>32#".#"32>32#'.#"#"57>54#"#"; 5J  F/ (F'  :7#M3 (I* %L2 G$ =9  #,%)5r!cP% "(!;$+Q.% );&)9tE'? D<# U /;h./ + +/ִ + + +01462";X|YX| |XX|Y1iq''T]./ +/ֱ +  + ++01747632#"'&'& dK >"z gf ?$'T]./ +/ֱ  + + ++0174654&54632#"&'cJ Zfgf]z 4>74>32#"oOr~  ׍H{ë   E% !2\)/ //30 2/3 2/ 3/1ִ %+4+1.99/%'99 990173>32.#"!!!!32>?3#".'#735B_Z*:Z-"O.;rU;v:$?,K]#FH<R7#/F\ $*2 $FM/ 0HK ==*O50' /  /+0174>3!2#!"& V   H_+Y3 g+c2 +  +^2,+8 8, +85 +#+ )++D33 P+KNqt$2/+6?/+   {?@+ fnRO  + +RPRO+QRO+fgfn+hfn+ifn+jfn+kfn+lfn+mfn+|{+}{+~{+{+{+{+  #99{999|9}9~9gfn9h9i9j9k9l9m9PRO9Q9@ Rin OPQfghjklm{|}~.........................@ Rin OPQfghjklm{|}~.........................@ o99I999!=998'901463232676?4#*"#"5463327>3232>32#"&#"32632#"&#"#"5463232>7>?4#"&+"#"32327>54.#"'     2Q!. )    96  *$       2(( Q ER  &)H$7 YG8     &<1&(  F14)#  / 'C?K :^1! !)18\ C[$`oHnW+I +IW +IL +l+ l + +*+3 3* +3/ +$+9?33!+;=33 9+_b2228+o/ִ +Y+F FY +FN +\FY+B \B +@\ +p+6?z+  g7  + + + + +g8g7+?+ eg7+  #9 9999eg79@ eg 7..........@ eg 78...........@l9Y%;b999\3_99BF/1>*$9IDY99!B993$%90143232>76754+"#"54>32;27>7>32#".#"32>232632#"54654&'"&#"#"5(    % &  4'!R&A #9  BV  & +"& E) P>  -!4" >*#/& +7LU5 y$0  S4  0%56Kgl>+5 +5> +59 + +f+ f + +!+H +P33 9+W2h/Aִ1 Q+1A +1; +1F+* i+6?r+ b]+ +?r+ + ++?+ b^b]+_b]+`b]+ #9 9 9`b]9^9_9@ `b ]^_..........@ `b ]^_............@1ADH99F!9*$-9951D99U99H-F99!*90146322>754#"#"5463;2>7>323263232>32#"&=4754#"32632#"&#"#")  ( ( 1_<    $# c* #*5%>" 62 /J!6251?#  2g2/R ,% !" b#"M $IdR  ĥ!dL`_+W +W_ +WZ ++u3 9+2+ +|21+@ f+@1 +@; +)+ /++$N333K+G$3g 9+22/aֱU Ua +U\ +dUa+P dP +@d ++6?+ $. ?mu+ .k p  $+ $+ $+ $+pmpk+npk+++++++++++++ $ #9999999999999999npk9m9@ km np........................@ $km np..........................@daK@99UL9P>1N999 y99gWRad999KP99%9@-9014>323267676754#"#"546;2232767>3232>32#".#"32>3>3232632#"54654&+"#".5463232>7>?4+"+"32327>4.#"'    (   Dj),$>#& $"9 (>*!+ \&( _%    -       J FR %&A&" rt:/ vP":    HbQ N*3 SO( F7:T,5 41&CQM><>;+=>;+?:+ MNMQ+OMQ+PMQ+nZnY+?V+ lnY+|}|+~|+?v+ |+|+|+|+}| #99999lnY9NMQ9O9P9<>;9@=MNln767654#"#"5463327>322>32326322>2#"=4?>54&#"32632+"&#"#"546322>754&#"&#"#";2>7>54&#"'     ( -  (29!" aHvS6 ;gK   || ./53@  $18\ ")2  `p g~+;3t 6+~+2 +2~ +@26 +x2 +M3% BE22+\/ 9+2  V /ֱq q +qz +q +_ +_=+. .= +@." +.8 +=. +@=G +.S+ 鱁+ qn9_9= \fl$9.@EMNVY$9S&+$9%28=filqz$9 @dn999V _99\9017467>4.546323263232632632+"32>32#"54654#"&#"5<>;267654&#"#"&#"#".#"32>32#" UC, RB- * !" `( h+=-   5&1"  '6P=?$9  (X2N# 3C     H^8 A#  5  M>R ,+4^lw+3 !+d  L+v+ 9+l+?+ 5+{+3 +{ + +++$+/+9+^/R +R^ +RW +h/o 9+3{ + g+'?+!,? +! M+/:ֱ m:+ /m +/ +/ +) +/+H +H+ 鳹+ 鲹 +x ++ 鲖 + ++ Q++@ 3?dhosv$9/N999^9R9999ohf9ds9Nm99v99993$999{H$9,!):9990114>323267>54.#"32654&#"#"54632#".54>3232>32#".#"#".732>54&#"463232>767632327>32#"54>54#"#"54654#"#" 7%F`,=K4Jq\@$?#&3'9*4,&;jC5R%$&67&:- .L)HuJ &N:/:$HZ!*  /x, 1c)Dl;& _VFg1) 4C,")A)%SP4@b?754#"#"546332>7>323263232>32327>32#"54654#"#"54>7>54#"32632#"&#"#")  ( ( 3_<  3"7Y    _#18"D  $5%>" 6 2  /J!6251?#  2g2/R  q.#S]N(/I  AV%IdR 8hdHW+h3L +LW +LR ++++3 g+2 +  +22+u )+ :+"+~33 9+222E+^ L++ 3 9+/rֱ: :\+G Y J JY +JT ++6?{+ yy+++?v+ +++ž+ #99999y9@ y..........@ y...........@:r5>B999YE^99W 9Ldj999^@ >BG@Ylo$9"99E9%767654#"#"5463327>322>323263232>32327>32#"54654#"#"54>7>54&#"32632+"&+"#"546322>754&+"#";2>7>54&#"'     ( -  B Q +*)I$7  #=8Z&v2  JuA!   q.#S]N(/K  AV ;gK  7pu ./53@  # 18\ ")2  `pK\+]q33T +T\ +TW + ++  + +!+| A++3 9+222/sְt2n lm22nD++6?r+ ?L+ t.xo+I+ ].`PJ?r+ + +?r+ + ++?W+ o,o++.o++/o++1o++2o++3o++4o++5o++ȱ+ JKJP+LJP+MJP+NJP+OJP+`^`]+_`]+olo++mo++no++?F`+ tutx+vtx+wtx++++ #9 9 9999utx9v9w94o+959392919,9/9.9KJP #9L9M9N9O9_`]9^9@& /12Jnowx +,.345KLMNOP^_`lmtuv......................................@% /12Jowx +,.345KLMNOP]^_`uv.....................................@ns!|999D$*HT\z$9T:H99D990146322>754#"#"5463;2>7>32326323267>3232632#".##"54>7654#"32632#"&#"#")  ( ( 1_<    yMM1  . F2&'   <5%>" 8 1 /J!6251?#  2g2/R  l*"! `$ 4D0X  8PESE   @Y2%IdR   ѣ"dHPg+y3_ +_g +_b ++++3 g+2 +  +22+ )+ :+"+33 9++ 3 9+2O+rHgO +r 9+/{ֱv vR++6?mu+ .+ kiV[WV[+XV[+YV[+ZV[+kjki+?N+ +++?v+ +++Ϭ+Ь+ #9999999WV[ #9X9Y9Z9jki9@VWXYZ[ijk....................@VWXYZ[ijk.....................@v{2$9R5F:_g$9_gw{999rv999Hp9Fl$9"R999O9%999.=>,$901463232>767654#"#"5463327>322>32326323267>3232632#".#"#"54>?>54&#"32632+"#"546322>767654&+"#";2>7>54&#"'     ( -  B Q +*)I$7  #=8Z&v2  JuA!   (P4 `$ 3D0Z 8PESE   @YKJ ;gK  Z ./53@(  !,o18\ ")2  `pKiy8+l "+ +h+ h + +!+F Q+3U 9+222+r L+TX22z/;ְ<2j y2jo+5 {+6?r+ d^?4+ <.Cy*+ +?r+ + ++?T+ y+y*+<=9?9@9A9B9xy*9w9+9@ @bd *+<=>?ABC^_`awxy........................@ @bd *+=>?ABC^_`awx........................@j;F9o!)28D$9Ul.5;o$9Q0990146322>754#"#"5463;2>7>323263232>32#"&54>7>54#"32632#"&#"#"32654&#")  ( ( 1_<  4*P359n1I 5%>" 30 /J!MBt9)%4 6251?#  2g2/R ./J5m+)D5W/y%IdR    d DX*< /(#_:HI+ "+q+++u3 g+|2 +  +z22+W )+ ;+"+`33 9+g2C+ L+2c+ 3f 9+/ִm 4+m +@me +mL+M2 鰷2+F 鱹+6?{+ ~p[?5+ M.S;?c*+ <;+MNMS+OMS+PMS+QMS+RMS+pkp[+mp[+?m+ op[+~~+~+?o+ ~+~+~+;+;+~ #9999op[9k9NMS9O9P9Q9R9;99<9@Pkmp767654#"#"5463327>322>32326322>32#"&54>7>54&#"32632+"#"546322>754&+"#";2>7>54&#"32654&#"'     ( -  D Q +*)I$MBs8)%4 7  #=8Z&v2  JuA!   #J5m+)Tm#u ;gK   > is ./53@ 'i18\ 0)2  `pDY*< /(#_:'>c%_+& 9+Y+N+W "+X2WN +WT ++ P+39_ +3 9+E "+d/ֱ# #+ +e+6?w+ X.-JGX,X-+JHJG+IJG+XYX-+,X- #9IJG9H9,-GHIJ......,-GHIJXY........@# 7763232632#".'.#"32>76?654'".543232>32"#"&5463232654"#".''H/VXFyB  &Qw3N . B (  "+4 !,N-cQ&g3LEG,$#"<)9<# 6U.   ^SK  ;T =k~ x+T 7+% +7+2 "+%7 +@%( +"27%+ g+  "++@ @ "+iax +i L+ai +@ad +7 + /ִ + +@ ++}2 + +@N +F +Q ++ +^+l +^l +^f +l+: %++99 9F$9Q@K$9x9=99^+.4T$9l!(7999aTQl99if}99@FKNH$9999%:$9 +$99.9015467>232>32327>54&/"&#"#"&54654&+"54632#"&#"32632327>7>54&#"#"54632#".5463235>7>4.'.54&#";2#"&732654&#" A<- >+1 + "  C8#D) Q ( !  d6Ym +U#!''  6%/.V<-K ,#8;  0?QX5)-*;{ZY+ d+Y + +4+:3$ "+>2$4 +$- ++F3 g+M2[/2ֱ' '2 +'* +'"+!2D %+C2D" +@DK +@D< +"D +@" +\+6?Q+ !. BT  !+ !+ !+ !+ !+ !+ !+TCTB+?(+ PTB+QTB+RTB+STB+ ! #9 9 9 999 9QTB9R9S9P9@ PT !BCQRS................@ PT BQRS..............@'2Y999"49D7N994$?6&#"#"&5432326?>54#"#".5432326323232+"#""!+  *$  R()  ;;   5 5C;4 >4Fh8==  ((V,   Q y%.{Cf4\5+/39 9+*2+B +B  5+ ++M+Z 6+MZ +MR +\+]/ִH +HP+U U + %+  +  +^+PHM9URZ99 7>$995-799Z&9M$%>H$9B!9014>32;254&54632#"&#"2#"&#"#"54>754&+"326&632#"&53 +޼  8&v &  KBE).18 5   !:Lf/      PS    =>(  PY|<+<+A 9++33R 9+T22#+ 333Z/ִ + + +[+ 9?BR$9A<?9R /5$9#(901432326322>54&&543232632#"#"&54>767>54'.'"&".!L/709&QP" >  F 5  ( !4&(    'StM 07 I  !;  Y&L'9'^"'>'S}'>{'V}'>'W};{'S9'^9'_'[e'R<'X<'S<aXgyV+D3 9+j2V +@VO + V +@  ++c 9+52c +@ +c& 9+!+z/ִY %+YT+G +TG +@T +@TQ +G+2; +:2;+ +o+A {+6?/+  _y7  _+ _+ _+?^+ y8y7+9y7+:y7+?^+ wy7+xy7+ _ #99xy79w99989@ 7_w 89:xy............@ 7_w 89xy..........@YW99T V999GO9h9;D]cj$9u9"599o&'>999A1*99c @ *.AY]ahou$9&'90174632327>54.54672>323#".'.#"#"#"54654#.327654#"32>54.#" 7  !..!sK  (7 7  '77'ze  >eo,:  $/h *5% #a C) &-;P "<'Z]&% "  % .>d& &M5_o* e D2' 5#H/0 $C J + + 2+/ֱ + C++  9999017432#"32>54&#"CjOQ;wM?IA3N&EB6P$xsΉJwoXqf21++.33 g+!2+ / P+3/ ְ2 + +@) +  +  +4+6?+ .+ #9.........@ !99 9 990174;27>54#"#"4>7>3222#"&#"#" - *%: #< K$J  ( %  '>>@b;+) +); +)1 ++ +  +  +A/ֱ  + +B+/38999 )9901754>7654&#"#"54>323267>7632#"&#"#"&>5B:1* 3 (F))," $ "U?  . =a Z  ?fZO6? % ,7) &>(0(1!,!% fH;p9+ }+(+ f+( +# +9( + "+7>54&#"#"54632#"&H.$H[;; $*-*2 d49N&.&%$a4V*xI7I  N+&7" +27> +@7/ +>7 +> +O/ֱ< <E+FG22 2E +@ + 2+ 1 1/23+ P+6?E+ 2.H* * +&* +?+ '* +(* +2E2H+F2H+G2H+(* #9'9@ (*G '2EF.........(*&'....@<99E1/4A999+K99%99 >K999017547>7>32;2>2#*&"#"#"54654+""&7;:62646554#"<2ez ' 7   ! uEC"'O :M /   &OM% %M  3HFzD+ g+&+ +0 +)+D +;D +; +G/ ֱ> > +@>( +H+> #99 >99;9 0901743232>54&#"#"&54>7>;232632#"&#"232#"&H4% !:.#I?$% ! Y  9;. ]OlPFR?+C!-h+% P++++ ++./ֱ" "(+ /+("999 99+%99 90147>3232>32#".732654&#"C3~1-M(#01IYrZ+E) VB9*=G137˞71  )(#\2Ks'32;2?2#".54>7654&#"&#"#"&#"#"z HG2 !k?3  >L[  R %. (@#IH4A 9~'J  H"3B +& "++@ 9+C/ֱ# #+4 C+#=+ 4+ ) )/ D+4# 99)@  &0:@$9@& 0:$90174>54.54632#"&732654.'"32654&#"H)(`TRW&&$%x_NkBJ4>O 5$*&2$(!E:6?;+H*<#UuX3*C# )A%cw^^NQX@'"&UN2" \0+ULF,o+ 9++* }+ " + "+ " +@  +-/ֱ '+ .+999'999* 990147>54#"#"&54632#"3267654&#"FAg6$ $9KORPh1Pdf/YC61 J2*> BGJ)Vge^nO$Yx(Nc^!9M[jY+,3 "+Y +@ +K/;3 "+^2K +@KC +K +@ +f/R +k/ִ ++N +NE+A +A= I +I/= +j2=A3+22+W2 +/ +b+8 %+l+6?X+ W.j.W/"j " W+j0j/+1j/+2j/+?_ + T W+V W+jhj/+ij/+ W #9T9V9ij/9h91909@ /TVWh012ij............ TVh01i........@NL99IEK9AC993=;RY^f$9/9,99b98(6!$9KL9f8\b$9R63999Y!%999990146232?4.54632632#".'.#"#"#"54>54#.276754#"32>54.#" %"F0  "    &.&U= $9L  -5 %&$ ) - (1F.   &92"8F  #& g ) 0$(T. I / 9+/ /ִ %+ + %++  9999014632#"732654&#"hE35[Jp3+&,7)&/6}\InEbQAXuI_5+ "++4/.3 "+$2/6/ְ2" " +@", +@" +" +@ +7+6?+ .   +  +  #9 9   ......  .....@" 1999419014;2>5>?4#"#"4>7>32:#"&#"#"I * + 88l @@   *% %^"2d+. 4+. +& + /  +  +3/ֱ (2 + +4+#+99.9 990154>7654&#"#"54>3232>7632#"&#"#"% N'%$7"'@6A6$-.%;5d&b>:0$ +-17![G?   [[3r+# P+# + +$+2/ 9+/ "+4/ ְ2/ %2 / + +5+29 /999 9+90143232654&#"#"54>7654&#"#"5462#"  /=** "5 4BR7++]B$p F) * !2!,&%2 &9`G!0=/+3 7+32 +@$ + +@ +>/&ִ 4+ ) %+)/ %++ +?+)&$+99!9;999 9999014656632;262+""#"54654#"#"&7;267654#" !  % { 5 ^    ;   ! U : .[#*<*+ +:/ "+: + +/ /2 !+=/ִ0 +0 +5 4+5 +@5" +>+0:$9  '299959 599290143232>54&#"#"5474>;23262#"&#"32#"&. *+# "$<  "+ 036 B,2z 4  B &D18;'Z"*j/" "+(/ "+( +@ + / "++/ֱ %+ Q+,+%999 999("990147>32#32>32#".732654&#"b"S!"A) 09L<$4 A("%,!zb!+92  O.F]#64 1L71;A<?V-/p%+ +% +@%. +0/ִ+ +++ "+ +1++999" 9 9%990167>32;2?2#".54>54+"#"? /.  $ 7C7 n" ' #4 &s_R  W"!.;/% "+,/3  "+9/ "+3.5462#"&732654.'"732654&#"?d6 N<1F7(","+&"$ & &2F6 ( /=J<54#"#"&54632#"3267654&#" *B#%12T75E1LU(B)  ,b '*, P4>]_>GtE%lhG .;K"4'/ִ + +  +  ++014>32#"."+0# -5- 9oO= 0AR4\1%.;R4'/ִ + + + ++01'4>54.54632#"-5- +0# /CR4\1%.;R)9oO=r8/ + +/ִ + + + ++014>54.54632#"  < )+ &&VRx#+ +/ֱ +014632#".'&/    ,   "2(&/ 9+/ 9+)/ִ +2 +" + +*+6?V+ .+++++++ #9999999@ ...........@ ..........@&"90174?6767>;2+"32632+"&"# & 3 * 6 %2!9#* $!ٜ  2*1'/ 9+/ 9++/ ְ2 +!2 +@ + + 2 + +@ +,+6?V+ .#   +?+  +  +##+#+#+ #+!#+?+ "#+  #9 9"#9999@   !"#............. "#........@ %99901'4326;2767>54+"543632+".3 . # '$ ٜ  %2:- @!v=.M=IL=%"M=N=!T=.#g="W=?-N="U= )S=""q="q=3r=x="#o=$o=._OI^O%"_O`O!fO.#yO"iO?-`O"gO )eOErOxO.~I~%"~~!~.#~"~?-~"~ )~tr?~x1~|E+?3I 9+:2+g +g  5+  +  +z+n 6+nz +nr +)3Er +R3) Y2}/ִk +kp+u ua+' %+'a +@', +@'= +a' +@aW +@aG +' + %+  +  +~+6?}+ L_6$%6$+LNL_+NL_ #9%6$9%6LN_$......%6LN_$......@pkn9uUYgrz$9aBR\d$9':9 3;?$9IE=BG9993P9),W99nk99g"a99014>32;254&54632#"&#";2#"&"#"2#"&#"#"54>767654#"#"54326;2764&+"&#"26&632#"&!.2  8&v 9Q! KC 1n  /[). 1 '4  <2*=      L     D    %7%'  J'/33 +22/+ ++6 + .=+ ++|+ +++ #999.....@ .........@01467>32#"'$8  -.    '''3hrz3hsz3htz3htzI`a:!/ִ + + ++014>32#".I1GM8 )>A8 +3+ $-(_f3 )E`c0"XR= (Nc`:!/ִ + + ++014>7654.54632#"-)>A8 +2+ $,(1FM8  )E`c'+XR= (NcE_f3?jF1,F*/! /3 -/ֲ222 %+2 +& + +.+6?+ .  + +?+  + + + + +  +  ++++++ #99999 9 999999@  .................@  ..............@!*&901465767>7>;2+" 32632+"&?7 (=V ) GS }=Hj,M)  ,i7 j1.M-/ 2/ //ְ2 %+ + + +0+6?+ . (++ + + + + ++(!( +"( +#( +$( +%( +&( +'( + #99 9 9 9 9 99&( 9'9%9$9"9#9!9@ !$( "#%&'..................@ !$( "#%&'..................@90146;267>7654#"#"543632+"%`) G^ 6 (>  #17i   =H&j,M) ;|6&/+ 2+2 +@ +0/1+6?Q+   +  +  +  +++ #9 9 9 999@  ..........@  ..........@901454&54676763232632#"#".#"#";+' $ &H)" % zq DC ! z  |&0 + +21/ִ +2 + +2+6??+ .'  + +  +  +''+#'+%'+&'+ #99 9 9%'9&9#99@  #% &'............@  #% &'...........@9 90147>5>4#"&543232632#"&#"#"$ % @% & /%  " x ym  l  A8mz'K}zz'zZ74>3!2#!"& V r  ~ 4632#"&~& $ !Q-&',m 4632#"&m$  O ("(*<"i 462#"4632"""0 " q4>32#".  0 P#: #?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghjikmlnoqprsutvwxzy{}|~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~     uni00A0uni00ADuni00B2uni00B3uni00B5uni00B9AmacronamacronAbreveabreveAogonekaogonek Ccircumflex ccircumflex Cdotaccent cdotaccentDcarondcaronDcroatEmacronemacronEbreveebreve Edotaccent edotaccentEogonekeogonekEcaronecaron Gcircumflex gcircumflex Gdotaccent gdotaccent Gcommaaccent gcommaaccent Hcircumflex hcircumflexHbarhbarItildeitildeImacronimacronIbreveibreveIogonekiogonekIJij Jcircumflex jcircumflex Kcommaaccent kcommaaccentLacutelacute Lcommaaccent lcommaaccentLcaronlcaronLdotldotNacutenacute Ncommaaccent ncommaaccentNcaronncaron napostropheOmacronomacronObreveobreve Ohungarumlaut ohungarumlautRacuteracute Rcommaaccent rcommaaccentRcaronrcaronSacutesacute Scircumflex scircumflexuni0162uni0163TcarontcaronTbartbarUtildeutildeUmacronumacronUbreveubreveUringuring Uhungarumlaut uhungarumlautUogonekuogonek Wcircumflex wcircumflex Ycircumflex ycircumflexZacutezacute Zdotaccent zdotaccentlongsuni0218uni0219uni021Auni021Buni0237uni02BB afii57929uni02C0uni02C8 gravecomb acutecombuni0302 tildecombuni0304uni0306uni0307uni0308uni030Auni030Buni030Cuni0312uni0315uni0326uni0327uni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni200B afii61664afii301afii299afii300uni2010uni2011 figuredash afii00208 quotereverseduni201Funi202Funi203Euni205Funi2060EurouniFEFFf_ff_if_lf_f_if_f_lc_tQ_uf_hf_f_hf_kf_f_kf_bf_f_bG.001 bullet.001J.001T.001Y.001i.TRKGcommaaccent.001Gcircumflex.001 Gbreve.001Gdotaccent.001Jcircumflex.001 uni021A.001 uni0162.001 Tcaron.001 Yacute.001 Ydieresis.001Ycircumflex.001 dollar.lining zero.lining one.lining two.lining three.lining four.lining five.lining six.lining seven.lining eight.lining nine.liningdollar.lining.supzero.lining.supone.lining.suptwo.lining.supthree.lining.supfour.lining.supfive.lining.supsix.lining.supseven.lining.supeight.lining.supnine.lining.sup parenleft.supparenright.sup comma.sup period.supbracketleft.supbracketright.supdollar.lining.subzero.lining.subone.lining.subtwo.lining.subthree.lining.subfour.lining.subfive.lining.subsix.lining.subseven.lining.subeight.lining.subnine.lining.sub parenleft.subparenright.sub comma.sub period.subbracketleft.subbracketright.subzero.lining.numerone.lining.numertwo.lining.numerthree.lining.numerfour.lining.numerfive.lining.numersix.lining.numerseven.lining.numereight.lining.numernine.lining.numer comma.numer period.numerzero.lining.denomone.lining.denomtwo.lining.denomthree.lining.denomfour.lining.denomfive.lining.denomsix.lining.denomseven.lining.denomeight.lining.denomnine.lining.denom comma.denom period.denomTbar.001hyphen.uppercaseuni00AD.uppercaseuni2010.uppercaseuni2011.uppercasefiguredash.uppercaseendash.uppercaseemdash.uppercaseafii00208.uppercaseparenleft.uppercaseparenright.uppercasebracketleft.uppercasebracketright.uppercasebraceleft.uppercasebraceright.uppercaseguillemotleft.uppercaseguillemotright.uppercaseguilsinglleft.uppercaseguilsinglright.uppercase equal.ref1i.ref1j.ref1 divide.ref1 uni201F.ref1$, @DFLTlatn0  AZE 4CRT 4TRK 4   aaltVaalt\casebdligjdnompfracxhistligalnumloclnumrsaltsubssups     $,4<DLT\dlt|TX2x4> ".26:>FNRVZ^bfjnrvz~>> *-7<>@V^`mo}!#%57Bpqrstu? $048<@HPTX\`dhlptx|>? *-7<>@LV^`mo}!#%57BpqrstuRL     ( >@( >@ rr V( >@^`mo}pqstu(*-7<!#%57Bl  (08@FLRX^IEINIKIOILKNEOLII XW4F F`DFLTlatnAZE CRT TRK cpspkern0 ( "$%&'()*+,-./0123456789:;<=c  !#%')+-/13578:<@BZ7x~$:Xn.8FLRd~$>Pbpv(2<  @` >a *x0@`  ) 0 @1BS`&w>|0 @ `p>k>'%?@`p )@`[ @-`">x  0/@>`5ew ># ?p )0@/`$>z] @`w`? ? ,? ' ? ?+7'` ".??>>6? ( ?? ?#2> .? ?>8>B?( 7C%? ?? )0`%7 6)01`7(K ='@L`A>gpH)Biw7 ")49>?T^egprx|>SFQ#B?66,6SS?" ; ;;""?&&&&& &&  && & &&&&&&&&&&-------------------- L6N@<"2D.:)),,NND;NNNNN;6N,(+N;1,( '' ' ';""""",,,,,,,,;,,,,N;;;N;;;;,,,,,,,,,,,,,,,,,mh<<SFQ#B?66,6SS?" ; ;;""?    $K23"2222 2222 2   2222222222      LAY@<=O.E44+)YYO;YYYYYEA Y 36YE<73('2 2!;-- ---##''4;Y;E;Y< <;; 9@G,,QQG QQQQQ=9Q+.Q=4+, ** %%%%% ,@@,Q=Q44 (((,""" DN :111NN:666:N ///:;S?;I-?..*SSI:SSSSS?;S -0S?-&&,,:'''''&&.:S:?:S: ::: SFQ#wwmB?66,6SS?" ; ;;""? %13==3 =====)%==)  11=)= 'l##5+??5?????+'??+" ##?+?"" 5*38BB8BBBBB.*BB.%  33B.B%%  DD;}nuQQQ`Qxxxxxxxssxxxx; ;;\333_3wwwww{{{mm0^e 3hhK^KKKcc^^^h; ;];]]SLLLVLnnnn^PTTT|aF+7>)).whqhhh)AFFFFhFFhhhhhwhhhh|hhz3KNNN(',!!!v-vCvbvU@@@dMdNdNdNd$dPw|~5~X~S0vb@y"luR G GWD8 8888%%%%%%%%%%%%%%%%%yyyyyyyygYZYZqq1$6^Pc$ 'Yo2lBlA8Uem99ooo1R<3JG=4JEE:GG) JELE5], [gh~~S|*wO# pp~IhT0ontt_j(l1~6fR&dolZo%z&1<<<<<<;J====EEECG JJJJJoJE+hhhhhhh~~~~~xkp~~~o~nnnnjHu<h<h<hJ~J~J~J~GG=~=~=~=~=~J}J}J|J|E*E">E EDrEqEw:G#G G G F GM p p pXJ~J~J~J~ETETEV505.505.]o]o`onnnnnntj,(,(,(S50]oNllllll\$i\\$i\{BdlQosvvy~-vyvyvyJbwJJJJbbbWWWWWWWWWWWWWWWWWa{{{{{{{{K7@?@/![![BDa?ci]kpdrujwyn{}qDtp8JLMOMgnslixmpp-1.2.2/docs/_static/fonts/YanoneKaffeesatz-Bold.ttf0000644000175000001440000035501012473141653024547 0ustar mathieuiusers00000000000000GPOS ]zJGSUB@Q'(OS/2UP``cmap C cvt  fpgmY7 sgaspg ]glyfϔITheadc6hheaf`T$hmtxrfkernJLlocaߢ@ maxpx name*H#post9[prep8_< |%`"7" 89dr sm`@ KYN @ 89  " #";" p !$& (fCr,'q~7/]oX| px^Wh ^r Rj#$nZ ~ i!!q!!!i!_l!p!> iJ{T,  WI   /8FC,,,,4pRRRRRR$ZZZZ!i_____iiiiTT!$#X>px,  r/ee`2    R]H rkM  r/_\k|v]Z:!&%   WCZEKn+r" 4F"&  ]##+l!]!n&   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYjkmov{[`^a]l}\eZ_dfizbhpgqnsturxyw~cHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcdHr@2";>]^z~1BSax7    " & 0 : D t !"!&"""""""+"H"`"e% #<?^_{1AR`x}7    & 0 9 D t !"!&"""""""+"H"`"d%qQE/+``F++y`ߝޫބޙ,rtvtzYZ[\]^_`abcd,K PXYD _^-, EiD`-,*!-, F%FRX#Y Id F had%F hadRX#eY/ SXi TX!@Yi TX!@eYY:-, F%FRX#Y F jad%F jadRX#Y/-,K &PXQXD@DY!! EPXD!YY-, EiD` E}iD`-,*-,K &SX@Y &SX#!#Y &SX#!#Y &SX#!#Y &SX#!@#Y &SX%EPX#!#!%E#!#!Y!YD- ,KSXED!!Y-+++2) +.% +)" +8,!+8,!++ E}iDK`RXY`ipz B    k+// /EX/ >YEX / >Y ++ и ии!и%и'и,и.01%#7##7#46;7#46;7>337>33+3#'37#G.2J.2.7'.2J.2-6JJ5I5I2$T2$3G3G2$T2$VT#e9p5+5и/9/EX/ >YEX/>YEX5/5>Y&+ и/01.54>732654&/.54675463.#"#41 5!&p><427+ 4&+ v ."42   !|79D R{ . :%3+!Q#3GY,>+4$+++EX/>YEXN/N>YEX///>YEXH/H>Y/9014632#".732>54.#"4632#".732>54.#"&'.5IE"2!GF"3!k    IE"2!GF"3!i     ~  5D#S^X)E5dY(I;"()!''^X)E5dY(I8"()!'' =`w>S_`//и/1`GиG/ZEX/>YEX / >YEX#/# >YEXYEXB/B>Y  +B]012.#"6454.'>232+#"&'#".54>7.54>.'326EV5(3  %%  * $E/6H,' )(@d3!!%0  ANW)  '9," $K&%! 1A%80'32-!  G5+$A=.;?"" +/EX/>Y01#".54>32$15*)50/1CLpT'.6yÍu8*F?"" +/EX/>Y014.'>32#"&'>t $04**52-5C&67'6.' $ Z v $ N   Z ( #% |  kw?+ и//+и 017#5#46;54633#.2k.2k2.k2.+ /+0174632#"&'>7.$G  D&I 8+  J +01746;#2.2. $+EX / >Y0174632#"&%")%"*@,(!*% f //017#>3/+/+  |X!P"//"и/EX / >Y+ 014632#".732>54&#"T_`U)G67E'z #(VU+*OqC9M/9\DQX6N|X#> + /EX/>Y и/и 01&54>32326767#!&5463<$--)  8   9F " '  * %% }|X1N )+EX/>Y,+,9и/'901.54>32326767#!.54>7>54&#"5 3@"UD&:I"J3  F:&  "%# /  HE-PR[8#-  7[LC !qX/7++* ++и/01#"&546732654.'>54&#".546325C$>S.61 163*8?=#B `NJA:+HFD_<'K?%DJ%/ !F3@X_&-L'+и'#/EX/>Y+и'01%26767+#5#.54>7>327?  -*#++& ! s "  *F f< :NYN;  ~@IGsX/K'+"+/ +,+ и/ и/01#"&'&'>32#".546732>54.#"s 4 ?6#;W7* 7 /  2),,X'| (MACmL*  '/9%2 <~$7{8/5/8и/+и/5 +(и(/EX/>YEX/>Y%+001">32#".54>32."32>54& 0!5,2$bZJ' %04>$X/+и/01".54>7#".50L4#- "4=,V6 XlD :/$~'6EF/4/и/Fи/.и/#и#/4CиC/EX/>YEX/>Y7+791@19012#".5467.'.54>.'32654&">54&(C1#  ,A,4G,5,! .>P  (+*() # &>.!5+#  B&$D6 5F&>^  0+B-I  6'//,*#"Z! 6#(|a-Y./"/и/. и /" +и/+(++0172>7#"&54632#"&5467726754.#"t,9"5+ B+ иEX/>Y +014632#"&4632#"&%")%"*%")%"*,(!*%,(!*% 7+ и /и/// +014632#"&4632#"&'>7.%")%"*$G  ,(!*%&I 8+  w ++01463!#463!#JJN2.2.2.2.  =].++&и&//!/EX / >YEX / >Y 10174632#"&2#"&'&'.5467>54&#".54><%")%"*U$5"$&  $  '/@,(!*%%2/C4/  "2(&+  (A`o +)+UD+i8+D@и@/UYиY/Daиa/EX/>YEX./.>YEX3/3>Y$+RF+=f+3l014.#"3267#".54>32#"&'#".54>3254656#".5>7>3232>"'"&#"32674)H751?&  # * /B2!=V &7' ) sEX / >YEX/>YEX/>YEX / >YEX/>YEX/>Y+01%##"&'632#*'3'.'  () " [ X ](e00e(\$/o0/)/0и/)$%EX/>YEX/>Y/+#%0146;2+32654.+32654.+ b=0!)"-G3r    "=1&:( 7,2VA%4+!E>"(=&C"+EX/>YEX/>Y 012.#"327#".54> &  ( 1 A9*?*%:G  "  YEX/>Y012+4634.+32>2J0Ze     F{`ȹ CP* >s"U+EX / >YEX/>Y+ 01%+47>;+3+  yfd#("' '"K+ EX/>YEX/>Y+ 01347>;+3+# yf"& 'X)s*//и/и*%и%/EX/>YEX / >Y+  012.#"32675#54;#".54>67  (&0 .G+9I+8R"" AeHM\3HlKeyS_//и/ ииEX / >YEX/>YEX/>YEX/>Y+01##4>3354>3#`-+-+`-+-+C  X /+EX/>YEX / >Y014>3#.*/* X5+EX/>YEX / >Y014>3#".546732>5.+DR$, ,  UU   $~"{+EX/>YEX/>YEX / >YEX/>YEX/>YEX"/">Y014>3>32#"'4.'&'#.+m&; r75&*.+ 3Æ .XPC 5+EX / >YEX/>Y0173+4>3 .)iU  9{://:и/ и /.EX/>YEX/>YEX/>YEX9/9>Y0146;>?>;#7>767'+'.'# UO  D ^/+  0./  ,'*54* X=k*0)73)  )38(0)k>%&//&и/ и/!%и%/EX/>YEX!/!>YEX/>YEX$/$>Y01.'#4;.=4>3+ -*Yh .)W 5;;f3 X'](//(и/ EX/>YEX/>Y#014>32#".732>54.#"0H01D*/G11E+   bfS" PgiU$QV;T6<^BA\9>fSy // и/и/ и/EX/>YEX/>Y+0146;2+#4+32> ^/I1!6D#-*,  /P=DW2_ 5r"6k7/-/7и/- #EX/>YEX/>Y+(2014>323267#"&'.732>54.#"0H01D*-/ ! fc%01/&/1 и /и/& #и#/*EX/>YEX/>YEX/>YEX / >Y,+)012#"'.'##&7634&+32>/I1'  %  .) /P=KY1E\: :ZA,  61 1I12/&/и2/и// /и/ #и#/EX/>YEX/>Y ##9) 9012.#"#"&546732654&/.546L< 2%*`)G6D? 2*[ Y!'{)''&F6!),*#-#z'$ DU rA+EX / >YEX/>Y и01##543!#.)zT TTTT }`//и/ EX / >YEX/>YEX/>Y01732674>3#".54>3' .+Z@;G% /,1.>`  7I*#GEX/>YEX/>YEX/>YEX/>Y01&6;>7>;#"&'.'&^2  + c   %OKBBKO%(D"Tci3wCiEX/>YEX/>YEX!/!>YEX&/&>YEX5/5>YEX:/:>Y01&6;3>7>;3>7>;"#"&/.'"#"&'.'&^1  + O3 * aw   K2E< K2ADD (D!l@DDDD@$Vai2u$GEX/>YEX$/$>YEX/>YEX/>Y01+'.'+&>3>?>3||&  & al81 .+ut?>fgBi<:WlL+и/EX/>YEX/>YEX/>Y01&>3>?>3#}61  .+y-+463374n jp/EX/>YEX/>Y01463!3267#!.5467#"% ,"       Y  W8V52+EX / >Y+ 01+3+4>3V m # 5#)'+  _ //01#&>3\/+/+  862+EX/>Y +014>;#4>;# m # "*,'+"  h +01463!# J2.2.,/EX / >Y01".'>32&-+   !() ,! #) 7(9:/ /:и/и/  и//и/ )и 5и5/EX/ >YEX/>Y,+#201467>32#".54>3254&#"..#"3267>< N43A$  E $ 0$$&#Y#~$//$и/и/EX / >YEX/ >YEX/>Y !014>3>32#"&'732>54&#"# ,#+* $8E (<p  0N8k}BL+N?E6%C +EX#/# >YEX/>Y#01.#"3267#".54>32 # $ =0'8$ 3@%.  #D9+:$ 6aNZk9K%a&//и&и/EX"/" >YEX/>Y" 01&#"3274>3#".54>32     ,#G1 :-%2 "E>-:# K 5 2]PMlC F"01/,/1и/& и /и/,)и)/EX/ >YEX/>Y& +#012#3267#".54>"267>54.YEX/ >YEX&/&>YEX / >Y и 014>;54>32.#"3++  ! 2>/5$ &-H3\!%9:G' ! *E!%} <y7EUV/F/и/Fи/V'и'/NEX/ >YEX/ >YEX"/">YQJܸ8>0123#"&'#*'#".5467.7>7.54>"32>54&4.'32>w  KW  2$@D.H33@# % ,?"  "!" ,QR  6;%A0 (#9.K05H+O+;5' +"0$/   % !L"}#//#и/EX / >YEX/ >YEX/>YEX"/">Y 014>3>32#4.#"#! +#5$.) -* !0 Z  !&&%EX / >YEX/>Y01;&+!]-6y+'и34/EX / >YEX/ >YEX/>YEX-/->Y4# 0014>3>32#>54&#4#">! +#6 !  ' 3/ .+     0& d<""3:C d/! /+EX / >YEX/>Y017#4>3-* ," !,-/ и /AO]A`]A]  AO ]A` ]A ]и/ &EX/ >YEX/ >YEX / >YEX / >YEX,/,>Y$и$/014&#"#>32>32#4&#"#  -)P-4?&-.( -+w #3 s !Lh//и/ EX/ >YEX/>YEX/>Y01>32#4&#"#! %+,&6#/* .)  2%Z' Q%]&//&и/EX/ >YEX / >Y!0174632#".732>54.#"VP(;&)>();'{      {:bLHe?7gU;C" #C:6@" "@!;V%~&//&и/ и /%EX/ >YEX/>YEX / >Y#01>32#"'#"&5732>54.#"!O($;*#0+ p    .O=e|D ,TG$- :O)*//*и/! и /EX/ >YEX/>YEX/>YEX / >Y &01#"&'5#".54>32.#"3267O 6-"#9K)"0r      "1XGYrBL,K:)3 !9+EX/ >YEX/>Y 01>32.#"#!G*)   .+ $223/'/и/3.и./ 'и/.и/ *и*/EX/ >YEX/>Y 9$* 9012.#"#".546732654&/.54>*1 +/ J [M+ -W&?   KDL  !  \8.! (p +и  /EX/ >YEX/ >YEX#/#>Yии#01#4>;54>33+327#".5E< % +"R < !) &  &  $5!H\//и/EX/ >YEX/ >YEX/>Y01%#".54>332674>3HO--:# /,  -+'8#T } CGEX/ >YEX/ >YEX/>YEX/>Y01&>3367>?>3#"&' 40/+d0  S%+,,+%S z   MEX/ >YEX(/( >YEX///>YEX1/1>YEX4/4>YEX?/?>YEXA/A>YEXD/D>Y01&>33>?>;3>?>3##"&/.'#"&'.'&2, J"*&   # " %S#)* "(*1>?=v n/ZF+BN''No=FJ#Ss'GEX/ >YEX'/' >YEX/>YEX/>Y01"#'.'#'&>3>?>3m63    .&uf3/  .* 0A@7=;$?O*<EX/ >YEX/ >YEX/>Y'01&>34>?>3#".54673267 /, /*p !,# % B9)(9C 1B'  #  "1 "&UEX/ >YEX/>Y 9  9 013267+&'.5467#".54637") Q&    #"  8W8/ "+ /EX/>Y01#4>3.+.+  ; =+/EX / >YEX/>Y 01#"&54632#>3%")%"* ">4 *!,(!*%  IMc1M(+-и-/1/"/EX/>YEX#/#>Y01.#"3267#5.54>75463# #* '42!  *42  "C:3:+ Lz6T?C^?$B~60+ии/0*EX/>YEX#/#>YEX6/6>Y+ + $и(и+и/017#46;5'#46;'&>3>?>33+3+#{Ri#N61.+ORE|S-+2*82**,)*++n 2*02*6   +и 014632#"&74632#"& # ## #s%"# %"#  ?%1CI2&+ +,<+7/+)?++#+01.#"3267#".54>324632#"&732>54&#")  ! !oghbjjjbR ..#8A/#   +$*2 mpibtlvos)9#"9+EI!7!C%EX/ >YEX/>Y0174>7672#&'.74>7672#&'. (  ! (  '  '  &,2; @ %('#? :2,& #'-4 :!#!8 3-'" ? =Gk +#+%>++%+и+/>1и1/#7и#A ++D6+014632#"&732>54&#"7476;2#"&'.'+"#74+;26 oghbjjjbR ..#8A/#D%O   J  pibtlvos)9#"9+EI!7HI +,((OG//EX / >Y01"&'&'>32+  ()    +-&/ )# !/U + +014632#"&/%")%"* ,(!*%""++01732#"&'&'67>7632654#"7^ /+      ;!&&8  p!C%EX/ >YEX/>Y01%&'.546767&'.546767&'.5467672.546767  '    '   ' &! '  &,2: ?#'(% @;2,& $'-3 8 (7;5.'#< =Y+.+&и&/EX / >YEX / >Y  101#"&54632".54>76.7>32326767%")%"*U$5"$&  $  '/,(!*%g%2/C4/  "2(&+  & >c& a& W& Dy& ]Z& q<#)#+#и%EX/>YEX/>YEX / >YEX/>Y%+!+01%+5##"&'>3!+3+'35 / (   yf] !d#(  "' 'B9~1"=HGD+>и>/4%+ +9++?012.#"32732#"&'&'67>7632654#"7.54> &  ( 1 1,/+      $4#%:G  "  1)&$as.&$%2y&$](&(>&(a!&(y&(]&-E&.>X&.a&.L&.9y&.]O4\7&'7'>76'B $#RR#$ BB $#SS#$ H #ZZ# II #ZZ# ,5BC/B/C и /B$и/ 5EX/>YEX/>YEX/>YEX)/)>Y0)=01".'&>7&54>32>7#"&'7&#"732>5T  /G0(=   /G1);Fa v    %&OfR# %.1#V5iU$,?fIT)TOD 1cW}&4>T}&4a}&4H}y&4]Kl&8aA3A+ (+  #и#/EX/>YEX/>YEX7/7>YEX:/:>Y 0014632#"&546732654.54>54&#"#"&'&'67>5$ZL7O2)1)MG?:  (0(!*&'* &OL(5"%1?S6BT" !=?D'"*6;X("  #7&?>17&?as7&?%A&?7&?](7&??DSaøb/]/b2и2/и/]]и/"и"/2K7и7/]ZиZ/EX/ >YEX / >YEX'/'>YEX-/->Y>-P>TиT/01467>32>32#"&'3267#"&'#".54>3254&#"..#"3267"7>54. ME@5 YEX/>YEX;/;>Y0!+5+E01.#"326732#"&'&'67>7632654#"7.54>32 # $ ,# /+       . 3@%.  #D9+:$ 1!&&8  f8]GZk9F&C>CF&CaF&C7F&C]:&>!&a&&&]M&LQ&M>9Q&Ma{Q&M-Q&MQ&M]0;wL+иEX/ >Y ++017463!#4632#"&4632#"&J #!# #!#2.2.q%"$!Z%"$! +4>?/1/? и /1# и / >2и2/16и6/EX/ >YEX/ >YEX(/(>Y,901.54>7.54632>7#"&'72>5'7.#"?  VP#5    )>("3l FF    H/{  $8 He? @ #C:  "@5H&S><H&Sa~H&S0H&S]3?O&Wax?O&W]-! /+EX / >YEX/>Y017#4>3/* ,# (89//9и/и(и)EX/>YEX/>Y!&+,и,/4и4/01%+5#".54>32>;+3+'32654.#" (?1E+/G0:%  yf&% d#(QldT$ "' 'uoxA^=Bh-AOP/K/Pи/K и/.KHиH/EX/ >YEX / >YEX$/$>YEX)/)>YE+)3=B0174632>32#3267#"&'#".732>54.#"7"267>54.VP"4: YEX/>Y +014632.#"#"&'&'67>5[[3C '(%LOG!% 4- !  0 ////01632#&'.'#"&'r     $ *     '/ //01".'>32>767| "&#    '%'"%     )* 5r  +014632#"& #+ ))-"%  ++014632#"&732654#"9341k33M 72+Y+01>3232>32#".#"#"&'&H  +   p,'      |J +017463!#J2.2.J +017463!#2.2.:+ //01#"54>32$G  {&I9+  ; +/ /014632#"&'>7.$G  &I 8+  q+ //0174632#"&'>7.$G  0&I 8+  :H)G*//*и/ и /"и"/ ////01#"54>32#"54>32$G  $G  {&I9+  &I9+  ;H )K*// и /*и/"и"/// //014632#"&'>7.'4632#"&'>7.$G  $G  &I 8+  &I 8+  Hq)K*// и /*и/"и"/ ////0174632#"&'>7.'4632#"&'>7.$G  $G  0&I 8+  &I 8+  z + +014632#"&/*3&.+4%%72*%4/&'u!5EYi}ֻjZ+bt+F6+>P+"+,+P и /EX/>YEX/>YEX/>YEXA/A>YEXe/e>Y9U+1AK9]иKoиUy01&'.54632#".732>54.#"4632#".732>54.#"4632#".732>54.#"   5D#SIE"2!GF"3!k    IE"2!GF"3!k    IE"2!GF"3!k      =`x>a^X)E5dY(I;"()!''^X)E5dY(I;"()!''^X)E5dY(I;"()!''!%EX/ >YEX/>Y0174>7672#&'. (  ! ( &,2; @ %('#? :2,&!%EX/ >YEX/>Y017&'.546767&'.546767  '    '  &,2: ?#'(% @;2,&  %EX / >YEX/>Y017#"&'>32-)    _C6+и/61и1/6:и:/EX,/,>Y +++,#1и5и:и>012.#"3+3+3267#".'#46;5465#46;>!(" uZh; ( >5$:*U)S4 )38_ (  #2*  2*! #"+K<2*  2*YEX/>YEX/>YEX?/?>YEXA/A>Y4ܸ;и<01476;46?>;"#7>767#+'.'##"##5476;#&$*   $Z Ephl)1 =@ 2(d&&wJ +017463!#J2.2. 7CD/6/Dи/и/и60'и02и2/8и69EX/ >YEX(/( >YEX8/8 >YEX/>YEX5/5>Y$+8и ии/ .и/и$>и>/01+#4>;54>32>32.#"3++'3547&#"\< !/80:7-$ &-H3\NN  }!%@7C$    *E!%}]9/   5ʸ6//6и/ии/ и /&-и&0и0/4 /EX / >YEX/ >YEX&/& >YEX/>YEX3/3>Y ,и-01%#4>34>;54>32.#"3++y/* ,# !"6E$?  4\ ]!%9:G' ? & =!%} /0//0и/и/ 'и *и*/.EX/ >YEX / >YEX/>YEX-/->Y +&и'014>;54>32+.#"3++  !!1:,P\  :%\!%9:G' \O!%}C8DRQ+(.+D?+(иQ0и7и.EEX/ >YEX / >YEXD/D >YEXQ/Q >YEX-/->YEX6/6>YEX>/>>Y+ и /&и'и/и0иKиK/014>;54>32>32.#"3++#+#4>3%467.#"3 !/81B#?  4\N\/* ,#  N!%@7C$   ? & =!%}}} 9& O<?L++ &+и&.и@и JEX/ >YEX-/- >YEXK/K >YEX/>YEX/>YEX%/%>Y: +ии'и(и:4и4/ EиE/01%#.#"3++#+#4>;54>32>32347.#"3<-*  3\N\< !/8076K   N{I!%}}!%@7C$  . O >&RX8Q!01/"/1и/и/" и /"и/"*EX/>YEX/>Y%+-01"&54732>=#".54>32.#"3265?B1" ' #- 8K,7^   )""6 9[ARmA PKh(C1YE"( 'l+и&EX/ >YEX/ >YEX%/%>Y +и014>;54>32&#"3++ !!+1H3\!%m'2  ! c!%}AM+++!и!//A/.+'(+'$и$/=и=/017.5467.54675463.#"26732>7#LD<(JA42-7++,# .<.%%!  !3"42; NH=G *"?T Bk6$&h4%#  AfUct+PV+/F+и)иP>и>//^EX$/$>YKY+4C+$0154>36767326767#".=#32>7#"&54>32'4&#"7>*  $   + ( 20)[  $!*9"PJ(B00&  6   $+{!)      !8,%+   pAiI(!2"!Z$ .2{LM/E/Mи/и/ и /Eи/$иE1EX%/% >YEX/>YEX/>Y+@6+%01#"&'&'67>54632.#"!#".546732>54&'.'7%[[+  '8%1J1( ($U4(  OD    + # $,0-S>% ,*'+   {>,2%+EX/ >Y +01<>3!#".546732>54&'.'7  8%1J1( ($U!% # $,0-S>% ,*'+   {9:/2/:и/ии2!/EX / >YEX/ >YEX/>Y/&+и01##4>;54>3!#"&546732654&'.'7.)< % +"8%,F2.)  )$U  &  # $,0-S>%, 79'+  ^()//) и /и #EX/>YEX / >Y +и !и #012+#46;4634.+3+2>2J0.H3"   2  Cv\i`.;2. CP* 2.>sM+и//EX/>YEX / >Y0173+.?4>3[ \ .)0/U'0/ G +и  //EX/ >YEX / >Y017#.?4>3! "-*  ,"0/'0/$ !FA+EX / >YEX/>Y+01.5467>323#!.546;5*++% KF;    C<T3+EX/>Y0$+8+и/ 85и5/014632654&#".547>32#"&'.546732654'&"#.\   , K9>:1@#)2  - &,    /*50(8$   !P(2 #+EX/>Y+&01.54>323#!&5467>54&#"5 09%1 '-#4)+6 &!<5. 9 "4,$h&]  + и  и / / /EX/>YEX/>Y+013+#"'5#.547>7>3270/: "$2  d ( B ?;3 &)]-VW/Q/Wи/Q;AиA/EX / >YEX/>YEX*/*>YEX#/#>YEXF/F>Y+6T+иF@01.5467>323#!.546;5#"&'>32.54>323#!&5467>54&#"*++% KF-)? 09%1 '-#4)+;      t &!<5. 9 "4,$D&'aA&'S:: \+ и///EX/>YEX/>YEX / >Y01#"&'4632#"&'4632% #% #itv!Np //017#"/.5467%   Xi" |)&?u //017'>32.'47   Xi" }(%// /017#"&'7'u %~Dk9;$+ и$7++/*+01#".54654&'.547>54&546   &3  W%(#X.   '.-Q#-&!$22S2&!  % 3f'=:k9;4 + и4'++*/+01.5467>54&5467.54654&'&54632&  %3   W&($W./' (.-P$,&"$2-+.&! 3f'<; P# ++01>3232676#".#"'. %+  $+        % %3E,+&и&/EX/>YEX)/)>Y -+01&>32'76'.7&5467#"&'>32  $ "   q r a'!    ! "T'! ! ~  C ?//и/  ++014632#"&732654#"C9341k33H $72+Y!+$+ и$01+.5#46;>32&#"3#}/2RH$ *m3L& 3EHH!2.vj/@2.+HXY/N/и/Nи/и/Y=и=/V%и%/=+и+/EX/>YEX%/%>Y %/012&#"#"&'.54732654&'.'.5467'.54>>54&'&'1. &2    ,92.%    +9     $  %5'/  %   %6 (/ }  &4{++'-+и/EX/>YEX/>YEX*/*>YEX-/->Y01%#"&'5#".54>32.#"3267#"&'4632Y# #4'4I*&Dq  % #  %4WBTpD HYa0< w!YEX/ >YEX/>Y+ +и ии 017#"'7#46;7#46;76323+3#$*AL" !,FQ"]N$@2.h2.[$S2.h2.g0(9:/2/:и/2 2и//+ 8+ ии/и/и/8&и&/85и5/0174632>32"#"&'#"&73267.'&#"732654&#"AH0==*YEX/ >YEX/ >YEX / >YEX / >YEX / >YEX/>Y01%#"'#"&'463232674632PQ0 &    (" "  &a+,z-//-и/и/&EX/ >YEX/ >YEX/>Y!(012.'>32#".5464&'.#"32>&>0. BI %A4.C,S < @[- >ԣ>mQ/?fLz5 2>3H"B>9EX/>YEX/>Y 01!+3#" hbe!(} 6>p / / и/ EX/>YEX/>YEX/>Y и ии01#"&'##"'#.5463!#g  B*# V S    %! / / и/ EX/ >YEX/>YEX / >YEX/>YEX/>Y и ии01#"&'##"'#.5463!#g  B*# V    %! ;'2+EX/>Y+$014>32.#"#".54673265 2>!'$ &- 2>!($ &-0:F&   *:F&   *  -f.//.и/и/!и'EX/>Y* +$+01>32#".5463254&#".54.#"3267, <449I&)5/ -66 1*D:!   !+!P"//"и/EX/>Y +014632#".732>54.#"KB!2 GE!1!]     od/P>ng0Q9$- -##- 2~34//4и/ и/и/'0и0/EX"/">YEX/>YEX/>Y",и-013>54&#"#.54>;.546323#  #  Xg.?%  8$KS^7q-QCvH 5I]8$MvR;`I3 $"fhB+/+01%#"&=#&5463!h   $oQ&;/EX/>YEX/>Y017>323+#"&'& $)H  2S     ;3z+и.EX/ >YEX/// >YEX/>Y ++/и01#".54673265#46;54>32.#"3# 2>!($ &-kW 2>!'$ &-Y:F&   *2.<:F&   *H2.l!C*7++01>3232676#".#"'&>3232>#".#".B-  "(B(  #+  M(      S%     ! +EX/>YEX / >Y 01>32!73# w|={ Ri%EX/>YEX/>Y01'7LLLde %2?LWC;+EX/>YEX8/8>Y@+&+014632#"'&>32'76'.7&5467#"&'7>.546776'&47   $ "   q r ^  "   q r   ! "T'! !   ! !S'! ! !'d%EX/>YEX / >Y012#".54>">54&#"&2#"&#"3263232654#"#"&=463232654&#"#"&#"3262654&#"#"&#";2/.#"#"&#"&#"#"&#";27'32654&#"#"&#"32632#"&#"3263232654#"#"=7632326732632#"&#"3263232654#"#"&=326=463R^33^RR^33^\#&,-'$+     k    F      !    9  9       3^RR^33^RR^3 &"$$"& ,,' p     p         b   " 8   ^"  $[ q     a) v 7 +01.546;# 7 !(/+0132>76#".5466 E8.$    3> (+] // //01>32#"&/>32#"&' *z  Y.]   (&}  = EX/>Y 01723267#".54>m    4  &    #2&#r"//EX/>Y01#"/.5467%463!#   8/Xi" |)2.2.#r"//EX/>Y01'>32.'47463!#   6=Xi" }(2.2.+*<=/+/= и /6и/+&и&/+.и./EX/ >YEX / >YEX/>Y1801#".54632&'.?.'>3274&'.#"32>H %A4.C,S@ Z 1  . ; M < g=i>mQ/?fLz-##.-   .-6 2>3H!8V+,/$/,и/и$$ и /EX/ >YEX/ >YEX/ >YEX/>YEX / >Y)01>32#"'#"&54>332>54.#"  $;*&/+) ,"    -O=e|D E  -VG(-S'(//(и/и/ и/ии/и!и!/EX/>YEX/>Y#++0146;32+#4#"#32>/$/I1!6D#-(,   x/P=DW2_ 5 9+/EX/>YEX / >Y0174632#"&4>3#%")%"* ">4 *!?,(!*% c ;5+EX/ >YEX/>Y01#"&546732654>3 6*:.$ ,"$ :, , G%_&//&и/и/ и/ /// / /// /01&'.67>32&'.67>32-!  G!  G5+$A=.5+$A=.L*RnXR: @ j T * (  8|".@68t$~r"BN~V"x 0 !Z!!"6""##$$$%&%&$&0&<&H&T&`&l&'x'''''''''''((( (F(())))*)))))***++++++++++, ,,",.,:,-2->-J-V-b-n-z-.<./ //"/./:///00>000011B122l22344J4x55567d7899:b:;T<<=0=>@>>?2?@@pA@APA`AABB4BCCRCDDzE@EF*FGGHHVHI:IJJ^JKK\KLPLLLMTNNOO@O|OPPQ4QQR>Rj` 2|  s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{    " & . 0 9 : ; </ ? A B C Hq K L M O P R S T U V W X [ _ f g h i j k l m t? w x y z { }              ) A B C E M O f g h i j k l " & . 0 3 4 5 6 8 ;{ B C D H' M O R S T U W m w x y z { } ~    $)2356789;DRTUVWXfghijkn"&.034568;{BCDMORSTUWmwxyz{}~  TTi "&).079:<=s?ABCDEGHKLMNOPQRSTUVWXZ[_bdefghijkl\mpqt'vwyz{|}'?TTT)35678;<= = ==  )7={?BCEfghijkpqvyz{==33H( )fghijkpqvyz{)3578H     " & . 3 5 6 8 ; B C D M O R S T U W m w x y z { } !! !! !)!3!5!7!8!=!R!T!V!W!f!g!h!i!j!k!l!p!q!v!y!z!{!!!!!!!!!!!!!!!!" " """""""&"."0"?"A"B"C"D"M"O"R"S"T"U"V"W"_"m"w"x"y"z"{"}""""""""""""""""""""""""""""""""" " "" " """""""""" ## # ## #)#3#5#6#8#=#?#f#g#h#i#j#k#l#p#q#v#y#z#{##################################$ $$?$B$C$M$O$P$S$U$W$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $ $ $ $ % % %%%%%% %)%=%?%A%B%C%E%M%O%P%S%T%U%V%W%X%_%d%f%g%h%i%j%k%l%p%q%v%y%z%{%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %% % %%%% &8&T&W&&&&) ) )))=)V)f)g)h)i)j)k)l)p)q)v)y)z){))))* **"*)*.*0*?*A*B*C*D*M*O*R*S*T*U*W*m*w*x*y*z*{*}**************************************++ + ++++"+&+.+0+3+5+6+8+?+A+B+C+D+M+O+P+R+S+T+U+W+_+m+w+x+y+z+{+}++++++++++++++++++++++++++++++++++++++++++++++.. . .. .).3.5.6.7.8.=.V.f.g.h.i.j.k.l.p.q.v.y.z.{............../ / c//c// /)/7/8/9/=L/?/A/B/C/M/O/_/b/f/g/h/i/j/k/l|/p/q/v/y/z/{////////////////////////////////////c/c/c//00 00)0307080='0V0l000000000011"1.13151617181B1C1M1W1l1m1w1x1y1z1{1}1111111111111111122)2=2D2T2U2V2W2l22222222222223 3 33333333 3"3&3)3.303=3?3A3B3C3D3E3F3G3H3K3L3M3N3O3P3Q3R3S3T3U3V3W3X3_3d3f3g3h3i3j3k3l3m3p3q3v3w3x3y3z3{3}33333333333333333333333333333333333333333 3 33 3 33333333333333 55 5555555 5"5&5)5.5=5?5A5B5C5D5E5G5H5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5_5d5f5g5h5i5j5k5l5m5p5q5v5w5x5y5z5{5}5555555555555555555555555555555555555555555555555555555566 6666666 6"6&6)6.696=6?6A6B6C6E6K6L6M6N6O6P6Q6S6T6U6V6W6X6_6d6f6g6h6i6j6k6l6m6p6q6v6w6x6y6z6{6}66666666666666676666666666666666666666666666666667 7777"7&7.707?7A7B7C7D7M7N7R7S7T7U7W7_7l7m7w7x7y7z7{7}777777777777777777777777777777777 7 7 7 77777777 88 8888888 8"8&8)8.80828=8?8A8B8C8D8E8G8H8K8L8M8N8O8P8Q8R8S8T8U8V8W8X8_8d8f8g8h8i8j8k8m8p8q8v8w8x8y8z8{8}888888888888888888888888888888888888888888888888888888899 999?9A9B9C9D9E9M9N9O9P9R9S9U9V9W999999999999999999999999999999999999999999: '::;{; ; ; ; ';f';g';h';i';j';k';p';q';v';y';z';{';';';';';{;{; ;{;{; ; ;{< =7======="=&=.=0=3=5=6=8=A=B=C=D=E=Hu=M=O=R=S=T=U=W?? ?D?R?V?W??????????????@@ @ @@@=@U@@@@@@@@AA AAAAABACAMAOAR AUAWA_AAAAAAAAAAAAAAAAAAAAAABBBCC CCCUCVCCCCCCCCCD7DuD mD "D DDDDnD;VD<OD=DADBDCDEDKDMDNDODPDSDUD^>D_D`?DDDDDDDDDDDDDDDDDDDuDuDDuDuDDDFDDVDDDDDDDDD^DuEEAEBECEHLEMEOEVEW EEEEEEEEEEEEE E EEEEEEFFFWFFFFFFFGHHH2IIIAICIMINIOIUIWIIIIIIIIIIIIIIIIIIIIKK KKVKWKKKKKKKLL LLLLLLMM M M MMM=MDMRMTMUMVMWMXMMMMMMMMMMMMMMMMMNN N N NN=N?NDNHNRNTNUNVNWNXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNOOOOOOOPP P PPP=PAPBPCPEPMPOPVP_PPPPPPPPPPPPPPPPPPPPPPPPQQ QQVQQQQQQQRR RR RRARBRCRERMRORRRSR_RRRRRRRRRRRRRRRRRRRR RRR R RRRSS SSSSSSTT T TTTTTT=T?TATBTCTETITKTMTNTOTPTQTSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUU U UUUU=U?UAUBUCUEUIUKUMUNUOUPUQUSUXUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUVV VVVV?VAVBVCVEVIVMVNVOVPVQVRVUVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWW W WWWW=W?WAWBWCWEWIWKWMWNWOWPWQWSWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWXX XXAXBXCXEXMXOXSXVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZZH][[ [[[[[\^_3_5_6_8`d d)d3d5d6d7d8dXeeH^eeeeeff f ffff"f&f.f3f5f6f8f;fBfCfDfMfOfRfSfTfUfWfmfwfxfyfzf{f}ffffffffffffffffffffffffffffffffgg g ggg"g&g.g3g5g6g8g;gBgCgDgMgOgRgSgTgUgWgmgwgxgygzg{g}gggggggggggggggggggggggggggggggghh h hhhh"h&h.h3h5h6h8h;hBhChDhMhOhRhShThUhWhmhwhxhyhzh{h}hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhii i iiii"i&i.i3i5i6i8i;iBiCiDiMiOiRiSiTiUiWimiwixiyizi{i}iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiijj j jjj"j&j.j3j5j6j8j;jBjCjDjMjOjRjSjTjUjWjmjwjxjyjzj{j}jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjkk k kkkk"k&k.k3k5k6k8k;kBkCkDkMkOkRkSkTkUkWkmkwkxkykzk{k}kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkl ll?lAlBlClElMlOlPlSlUlWllllllllllllllllllllllllllllllll l l l l m m mmm"m&m.m0m?mAmBmCmDmMmOmRmSmTmUmVmWmmmwmxmymzm{m}mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm m mm m mmmmmmmmm n nn?nBnCnMnOnPnSnUnWnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn n n n n o oo?oBoCoMoOoPoSoUoWooooooooooooooooooooooooooooooo o o o o p pppp"p&p.p3p5p6p8p;p?pBpCpDpMpOpPpRpSpTpUpWpmpwpxpypzp{p}pppppppppppppppppppppppppppppppppp p p p ppppppp q qq'qq"q&q.q3q5q6q8q;q?qBqCqDqMqOqPqRqSqTqUqWqmqwqxqyqzq{q}qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq q q q qqqqqqq t7t Bt7t7t7t7t7uuuuuuvv'vv"v&v.v3v5v6v8v;vBvCvDvMvOvRvSvTvUvWvmvwvxvyvzv{v}vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvww w ww w)w3w5w6w7w8wVwfwgwhwiwjwkwlwpwqwvwywzw{wwwwwwwwwwwwwwxx x xx x)x3x5x6x7x8xVxfxgxhxixjxkxlxpxqxvxyxzx{xxxxxxxxxxxxxxyy y yyyy y"y&y)y.y3y5y6y7y8y;yByCyDyMyOyRySyTyUyVyWyfygyhyiyjykylymypyqyvywyxyyyzy{y}yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyzz z zzzz z"z&z)z.z3z5z6z7z8z;zBzCzDzMzOzRzSzTzUzVzWzfzgzhzizjzkzlzmzpzqzvzwzxzyzzz{z}zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz{{ { {{{{ {"{&{){.{3{5{6{7{8{;{B{C{D{M{O{R{S{T{U{V{W{f{g{h{i{j{k{l{m{p{q{v{w{x{y{z{{{}{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{}} }}}} })}3}5}6}7}8}?}A}B}C}E}F}I}N}O}P}Q}R}S}U}V}W}X}f}g}h}i}j}k}l}p}q}v}y}z}{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}~~~~~~"&.3568;BCDMORSTUWmwxyz{}"&.3568;BCDMORSTUWmwxyz{}  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}  DRVWDRVW DRVWDVWDRVWDRVW DRVW ABCMOR UW CCUVCUVWCUVW  F 'FVW'''''? FVW????? VW DIRTUVWX DIRTUVWX DIRTUVWX DIRTUVWX DIRTUVWX DERU W   VWVW ?ABCEIKMNOPQS ?ABCEIKMNOPQSVW  )35678?ABCEMOPSUVWfghijklpqvyz{   DIVWX"&).3568;BCDMORSTUVWlmwxyz{}VW  "&).02?ABCDEGHKLMNOPQRSTUVWXfghijkmpqvwxyz{}'"&.3568;?ABCDEMNOPRSTUVWmwxyz{}?ABCEIKMNOSVW $)2356789;DRTUVWXfghijknopqvyz{ $)2356789;DRTUVWXfghijknopqvyz{ s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{ s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{"&.034568;{BCDH>MORSTUWmwxyz{}~ s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{ s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{"&.034568;{BCDH>MORSTUWmwxyz{}~"&.034568;{BCDMORSTUWmwxyz{}~358 )35678DRVW 7u m  G;V<OABCDEKMNOPSU^>`?uuuuFV^uVW7u m  G;V<OABCDEKMNOPSU^>`?uuuuFV^u   s "&)t.03 568;(?ABCDEKLMNOPQRSTUVWX[efghijklLmpqt/uvwxyz{}~7'?{! *7*Lv | u   "/Qk  ,A m >{ ,  * T)  }   8 8 D 43 "g Copyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone KaffeesatzBoldYN: YanoneKaffeesatz-Bold: 2010Yanone Kaffeesatz BoldVersion 1.002YanoneKaffeesatz-BoldYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone Kaffeesatz BoldCopyright (c) 2010, Yanone (http://yanone.de/typedesign/). All rights reserved. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFLYanone Kaffeesatz BoldRegularYN: YanoneKaffeesatz-Bold: 2010Yanone Kaffeesatz BoldVersion 1.002YanoneKaffeesatz-BoldYanone Kaffeesatz is a trademark of YanoneYanoneYanoneYanone Kaffeesatz was first published in 2004 and is Yanone's first ever finished typeface. Its Bold is reminiscent of 1920s coffee house typography, while the rather thin fonts bridge the gap to present times. Lacking self confidence and knowledge about the type scene Yanone decided to publish the family for free under a Creative Commons License. A decision that should turn out one of the best he ever made. It has been downloaded over 100,000 times to date from this website alone, and you can witness Kaffeesatz use on German fresh-water gyms, Dubai mall promos and New Zealand McDonald's ads. And of course on coffee and foodstuff packaging and cafe design around the globe. In 2009 he reworked much of the typeface and it got published in FontShop's FontFont Library under the new name FF Kava. You can read more about it in an extensive article by Yves Peters on the FontFeed.http://yanone.de/typedesign/http://yanone.de/typedesign/SIL Open Font License, Version 1.1http://scripts.sil.org/OFLYanone KaffeesatzBold`  "#$%&'()*+,-./0123456789:;<=>?@BCDEFGHIJKLMNOPQRSTUVWXYZ[\]_bcdefghjikmlnoqprsutvwxzy{}|~     !A^`a.nullperiodcenteredlongsEuroffffifflt_zg.ss01f.ss02ampersand.ss03ampersand.ss04germandbls.ss05z.ss05t_z.ss05uni2074Deltauni00A0macronuni0237` :latncpspkernHVQh`vs tH.@N`rx~$*< F T 8 J,V<vfJT6v4N`8J(:@ | !Z""#X$$%>& &'<((() )**+t,b-P.:.H.V./01181j1122B22223 323D3^3x334.4|455b5t5556h77 8 8\99$:Z;<;<==>@D@B8CD,DDDE`?uuFV^O O =# = =O O - =IKOP, =IKOP'IOP+ =IKOP#O* ;Omwxyz{}* ;Omwxyz{}* ;Omwxyz{}* ;Omwxyz{}* ;Omwxyz{}* ;Omwxyz{}$ OP 8 0Omwxyz{} # OP # OP 6 ;OPmwxyz{} 6 ';OPmwxyz{} 777)';Omwxyz{} =fghijklpqvyz{ =fghijklpqvyz{; ;=Ofghijklmpqvwxyz{}; ;=Ofghijklmpqvwxyz{}; ;=Ofghijklmpqvwxyz{}: =IOPfghijklpqvyz{);Omwxyz{}(;Omwxyz{}M 0=KOPfghijkmpqvwxyz{}      O  '''??? =I =I =I =I =I = + =IKOP, =IKOP: OPfghijklpqvyz{  I*;=Olmwxyz{}M 0=KOPfghijkmpqvwxyz{}8';OPmwxyz{}&IKO/7u " n;V<O=KOP^>`?uuFV^ ;fghijknopqvyz{ ;fghijknopqvyz{Us0;(KOP[efghijklLmpqt/uvwxyz{}~7'?{Us0;(KOP[efghijklLmpqt/uvwxyz{}~7'?{'0;{Omwxyz{}~Us0;(KOP[efghijklLmpqt/uvwxyz{}~7'?{Us0;(KOP[efghijklLmpqt/uvwxyz{}~7'?{'0;{Omwxyz{}~'0;{Omwxyz{}~;fghijkn,7u  G;V<OKOP^>`?uuFV^07u  G;V<O=KOP^>`?uuFV^Us0;(KOP[efghijklLmpqt/uvwxyz{}~7'?{,`(Z.80^lz,V\Rd$V"`*   "&)t.3 568:<?ABCDELMNQRSTUVWX )<ABCEM <"&.34568BCDH'MRSTUW* T "&).79?ABCDEGHLMNQRSTUVWX_dxT )35678  )7<?BCEOl3H( )l )3578H )378V '  '<l'{{ { <1"&.3568ABCDEHuMRSTUWmwxyz{}W <VWH] <H^ "&.03568<BCDMRSTUW "&.03568<BCDMRSTUW "&.03568<BCDMRSTUW "&.03568<BCDMRSTUW "&.03568<BCDMRSTUW "&.03568<BCDMRSTUW?ABCEMSUW  "&.?ABCDMRSTUVW  ?BCMSUW  ?BCMSUW  "&.03568?BCDMRSTUW  "&.03568?BCDMRSTUW   B<B777#@\H0>L6Tz,B`~6p*H^ X  T @ < BL"&.03568BCDMRSTUW  )35678<V  )35678<V  "&).035678<BCDMRSTUVW  "&).035678<BCDMRSTUVW  "&).035678<BCDMRSTUVW )35678?ABCEFNQRSUVWX"&.03568BCDMRSTUW"&.03568BCDMRSTUW$ "&).2?ABCDEGHLMNQRSTUVWXl < <DRVWDRVW <DRVWDVWDRVWDRVW <DRVW <ABCMR UW <CCUVCUVWCUVW < <F I FIVW''' <FIVW??? <VW <DRTUVWX <DRTUVWX <DRTUVWX DRTUVWX <DRTUVWX <DERU W  < <VWVW?ABCEMNQS <?ABCEMNQSVW )35678?ABCEMSUVW  <DVWX"&).03568BCDMRSTUVWVW$ "&).2?ABCDEGHLMNQRSTUVWXl"&.03568?ABCDEMNRSTUVW?ABCEMNSVW !#$%')*+-/12356789<DRTUVWXl !#$%')*+-/12356789<DRTUVWXl  "&)t.3 568:<?ABCDELMNQRSTUVWX  "&)t.3 568:<?ABCDELMNQRSTUVWX"&.34568BCDH>MRSTUW"&.34568BCDMRSTUW358 )35678<DRVW < mABCDEMNSUuuuVW mABCDEMNSUuuu <40  cc   uuum L2 t t t>q 9f{}0678 &)+.35=$?I-KX8Z\F^^I``JeqKt{X}`f|( 0:;<=FKOZ[efghijklmnopqtu v{}#*16:;=>  &)+./135:<<?ACF!HI%KN'PX+__4dd5fq6w{B}}GHIU[bjrsuvx)23+*+11 23 !"#$%&'/0 """"&& '**,-.,-.+/0*()-$$..........+/#-()*(%%&%'  !".....////,))))% )+'#!,"  @ latn  aalt>dligFhistLligaRss01Xss02^ss03dss04jss05pss06v   (08@HPX`hHFPb`^\Z^|Y|Xp GJ\PVPP&YH&QRDEXDEQXslixmpp-1.2.2/docs/_static/fonts/OFLGoudyStMTT.ttf0000644000175000001440000052272012473141653022764 0ustar mathieuiusers00000000000000FFTMU#]GDEFdP4GPOS!GSUB޿NC:OS/2X|Vcmap &cvt (fpgm/eglyf‹5xhead|?6hheaT$hmtx<' locaBmaxpVx name({postB AprepV̶<a =I_<nnJRJZfRlr1oBPfEd@  !ZMD8;@;;8FIF ;6T,6'T' V,'';;))^K5,,v',\*2,H"[U,4G-T;/'V3NMm0# '5''Y'K$!;O#'+/'w7V@ $z(*N |;'-N;g,.hT,qQ*Q( ,MQRkh'RR(^$,,,,,,',\\\\HHHHU,U,U,U,U,XU,////80000000'''''$ #'O#'#'#'#'#'#'@@@@ ,0,0,0','','','','5'5'\'\'\'\'\'2,'2,'2,'2,'KKHHH H$H$j0$"![[[[[OOO98U,#'U,#'U,#',['wwwT;7T;7T;7T;7VVV/@/@/@/@/@/@V3z(3z(3z(=T;7VkkjHpMdT,T,I88II88J-?-;;6'sf_$(' DG`f>B D 'OU'FOJ'I''T@uK'@*B<*A/M39$FO' ''D'J'TD ''OJ''T'D'OJ'TD'J'TDTD 'U'J''TuD'J'T ''' 'OO''D'J'TJ'TOOU'D'TQ"QQRQ*Q(QQ/QQ8Q$QQ"QQRQ*Q(QQ/QQ8Q$Q'}'}'}'}QQRQ*Q(QQ/QQ8Q$QQQRQ*Q(QQ/QQ8Q$QT,T,T,T,FIF N* &hh''J't < ~7I7 '  " & / : > D ` " 9L7 &  & / 9 > D _ "( srPONIG7_]ZRIFA'w   !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a}rdeixpkvjsgwl|cn?m}brsyzuv7~yw{HNqKLMzOI,KPXJvY#?+X=YKPX}Y ԰.-, ڰ +-,KRXE#Y!-,i @PX!@Y-,+X!#!zXYKRXXY#!+XFvYXYYY-, \Z-,"PX \\Y-,$PX@\\Y-, 9/- , }+XY %I# &JPXea PX8!!Ya RX8!!YY- ,+X!!Y- , Ұ +- , /+\X G#Faj X db8!!Y!Y- , 9/ GFa# #JPX#RX@8!Y#PX@e8!YY-,+X=!! ֊KRX #I UX8!!Y!!YY-,# /+\X# XKS!YX&I## I#a8!!!!Y!!!!!Y-, ڰ+-, Ұ+-, /+\X G#Faj G#F#aj` X db8!!Y!!Y-, %Jd# PX<Y-,@@BBKcKc UX RX#b #Bb #BY @RX CcB CcB ce!Y!!Y-,Cc#Cc#-KPXYF+X!YKRX!Y+\X E+D E+D E+Fv+D E >+Fv+DY+B,GGQ*I2^K7E (,&NV2p  V l l F TR< "0jRf$ !N""#$%&2&&'6'''))**+X,- .../0Z12234j556L6789:F:;V;< ?A8ABfBpC,CCDD"E8EBEFF FFFGGGGHTJUUUUUUVVVVVVWYpY|YYYYYYYYYYZZZ[[[[(\]T]`]l]x]]]]]]]]]_b```````abnbzbbcccc*c6cBcNcZcfcrc~ccdexeeeeeeeeeeeffgmJmVmbmnmzmmmn2oo$o0o–DV\z8v^P$ȣLԥ^親\Ҩbxȩԩ(4@LXdp|ĪЪܪ $0+"9%:99 &(7$9 *-3$9/901!326323254&54>54&#"2>7>54&/.#"N $@\,4,P6(@     $@ )'$8  #XDT!6<,     D2Q++3/ִ" +,"+)+4+,$9)$9" 99901747>32"&'.432"5.'&D      1  ?      T& "w gy~8z;&ewd/ET33>h22d +@dM +\2/7s33$222 +@ ++2x/^ְ_2[+[++ 2"++/"+!2f"++/ 3f+f +@ +22a `3  3O+P2K+RKO+Q3K)+-+.20-)+/3y+6?hM+ _.Y  ?`+ P.'J ._ _+?a+ _+ _+_+Y!Y +J/J.+?f<+ :J.+HJ.+PQP'+_`_+?_+ PkP'+lP'+mP'+nP'+oP'+pP'+YvY + _ #9vY 9kP'9l9m9n9o9p9HJ.9:9@':HJYkv !./PQ_`lmnop.........................@':HJYkv lmnop..............@a^c99[\9 99"9ROM9)K>Fq999089-+2990170546;25>54+"&54;256767632;2767632;2+";2+"#"54654+""54654+"7;27>54+"; R _ f bEQ_b  B$F_ b D/ dC    #3D  /. @j[h{ W+E3 }+k2W +@WP + W +  +5/f3(|/ִ\2+\a+2:+72:y +/ U33y+Ki22y +@ +a&+&a +@&. +:q+@2+}+\99 RWf$9a9yP99:H9&Gkw999q)15=>$9@095 .0@^w$9(90174>323254654'.467>54&54323"'.#"#"54654#"&'&3254654#"32>54.#"@ B $;KbC 6:  ZW!87  /f Ri  O)"#&`% - *@9 Jd    1 %=T\ `P2P0?E - T,n* = >)25;;GS_Q/W9+9/?"+]/K9+,/"+E/"+E +@ +`/ֱ<<B+1N+421H+TTZ+Na+<9B 9$919H/9T,99Z *KQ&$9N99WQ99 HNTZ$9,K4?632#"&5476?>54#"#"."#"&732654&#"4632#"&732654&#";sT 1,.%/ ޑ 5k*%9 6X#KH 0:8?nX66nK;>G -@8?XQ#"%  O( + M:9H-PAO7>M(4qUD7WQF7@K(9u;je+p+Y+Q+!P+_+e +_+=Ae +="+/ֱkk +$$ +$ +$/+H$+H/ +@H? +LH/+t "+t/L "++$ 999t57ep$9/+:_a$9QYV9_pa9+Ltv999A/Ek$9=59! $~$999017467>54.546322632#".#"32>54.'.546323232632#".#"#".732>54.'.#";=%tH#> "9/.794K   3M  w= 9('H2&&6Z6 @YC/B"AtH+S*4x> !L"I ;&BC  $+$1O<4Kx(%-   , K   6fe#LI/'f> T8vI*<!/ִ+ + ++014>32#".I*54.54632#" )::)(9:( 1?:'*;2=4>2;2+"#"&=4&+".          6Z}./ +/ֱ + + + +0174>7>32#"54>54.'.6  "Y 6  >'?g %  ,|( /33+2/+ ++6#+ . o+ ++++++++ #9999999@ .........@ ............@0175467>32#",&5R #7    "    65++++/ִ + ++01647>2#"&'6!   + '   #Ir+/+6<+  +  + #9 9  ......  ......@01477>32#"1  77  $ ' P +8++O+/ִ 2+ +$++  99990174632#"&732654&#"'[]tgqw8jFDbkFE`erqlZXQW`OJXW@=y<+<+9+22+9++"+>/ֱ.. +@.4 +@. +.?+.9:999<49 99990174>76=4654&'"543232632#"&"#"" 56<(9 ;6 [.Q  61P ">   " Q 6{2+"+.+53+ 9+  +  +7/ֱ   +  + + 8+  "2$909".9 ')$90174>7>54&#"#"&54>32327>32".#"#" "phC!; -"H0;KO2=.d*# 2RGAE C5+4. *  "'84+f)2 - _'4f;7+f+7 + +&+& +! +7& +9+7>54&#"#"54632#"545'.!;PRB    07% ) P,3O#)#&H[U a60@  B(1 5+2;& $HS$  FFY=+@3M +I22$+<>33+ +Z/ִG !+GP+;2*26 6/PR R +@ +[+GC96=@MV$9PT9R49 9M= 999G999 RTX$901746767>3232>32#"&#"#"54>54#"#"&#".7;23265654" /w    (  f#C ]&% 7  8 C( 3     K D6E 9; I/3BHvD++4+4 +@) +++I/ִ:$+: +@@ +@+ +J+:99 ;94@$9.990143232654&'.54>7632;:>7>32#"#"'./ 1#6Q'/7    /N .K M/9 I<  ! # C)UM/,2m+%9++- /"+3/ֱ (+4+(  $9 999-%$99  901467>3232>32#"'&732654.#",[%W ,>*,A9%Wfk[QG;Q./,J$D+"+r? $%n@vc_I>Df6RKDF0)D,8+ 3+ +@( +-/ֱ.+990144+"#"546563232>32#"&#"u=N$-=# +'9H  ! #  M 2v- '"1=+&9+,/52+;/ 9+>/ִ#+ 22+#)+N+8 +?+82 &,$9,&995$9; 990174>4.54632#"'.732654&#"32654&#"'''#*#gNW^*2*heO+-2,_HJJ!,%"!KI@1Q(>"  @&Md[H!6 #I-R]:K`Y5@u:<4ZK<(YB'3*m +9++(P+  +"+ +@ ++/ֱ%+,+ 9% $9( 990174632#"54327>54#"#"&732>54&#"'p]_lZY<^32#".'&47>2".'.;#         7    &  %     ;X8Z++9/4ֱ*4* +40 +*  +/3 +:+4$999 990147>32".'.4>32#"&54>54&'&;      \ V  &       ,?i +!))}74>767>32#"%.)җ$ **6 k L8  op ZJ&kb))}747%654'%&5432#"&) 7**0 k A po 8L K98h4+$+/ + + +9/ִ +2 + :+ "$4$9 (/3$9,9014632#"&54654&#"#"47>32"&'.K@(6P,4,\@$ $     d,<6!TDX#  8$')      , EU7+CP+7C +7; +S+9+S +@" ++3+ +7" + I 6+1/g+V/ִ42+4+FF.+ +W+.F@  17=CP$9S+@ &.4FP$91$9014>32#"."#"&54>323263232654&#"32>32#"&7326767654&#",A_xa*[ &$;/.?n:3 )Lbtѱ?f4 ,9Q+#6 (,1k`W9m$5,9] (ax! ;sl{ɡ! i"/JW+!CI3339+ +U +2NI +2L+X/)ִ +) +)$ +Y+)9$@9992')>$9UN99 9901747>767>32#"&#"#"&54>54&'&#"+"#"&#"#";7254&#" 0_bK    * @<2 N&5-   97 !_'b ` SԠ6O"#       9# V  Y;Xj +>'++"++ 3:"+g2aJ +a9+k/5ֱY T99a99:90143232632632#"&#"+"5432632>76=4.#";27>54.#"&"&#"3;2654&#" wF[z"0/"2",L6( 9  'b6 /=$.?# )  @??iaWP3   $G0+b  -#A<R91G$!>>%.28B48i%B,5K0+  0 + $ ++0+6/ִ +7+ 99 99014>322632#"'&#"327632#".,Tx=|%%  a}%KXb  6=^1WY<Dmk$ A*G|g< %- *D[]/Sn+3;P+2+3E,2T/#ֱ0 #0 +@# +@# +0@+ U+0#99@99E; )#9990146;2632#"&#"#"54>7>54654&54&'.;267654.#*"W$EtH,,&r<*#<!  $ #&D`,J?fv<!  *ONFgK/ * '8)$A#    '*GPM) #<Hs4[+L^+X3d9+d^ +@dS ++ +3M++3r9+ +@ +,+"BX, +"B" +@B7 +t/oֱ DG22o +@o +@ob +9+5+5/ *+*//+5++ +@ +u+o[99BL$95*,7X999 9d[`9BL2Ui999"j9, /99m99 r 99014323232632#"54&'&+";26767632#"'&'&#*&"#"32>32#"&#"#"5<>7>=4654'.'"; "Z04*<(  K% )Kq9&  V HT 0 C ,.H_2  N"*W(H+X  Fp|r @ u"[+UP+a+e"+a+++ 3t9+ +@ +1+"I[1 +"I" +@I< +v/oֱ KN22o +@W +o +@o +@oc +>+9+329/+//9+ ++w+oQ^99>)U[$99/16<999 9e[Wc99IU9fg999"6k991 3999t 99014>;232632#"54'&#"2326;267>2#"'4.'.+"32#"&#"#"5467>74546=4'."& kVGn. .-<'  H W)'! E E " /  -B "6q1   87  W(,  Y5s :B,TP+ +}+/)P +/9+62U/ִ +%+= B22=% +@=8 +%= +@%- += +/+V+%/P$9 2EH$9=9:9) :H999/258999  $9014>3232632"'.#"3267654'&'&'&5432326;2#".,W|0T6(#W}DZM}  V .3" "%mX*Imk@* Li\jF-f'    +E.  ;evw+FL}333r"+2w+A"+/+)3332"+ &222g} +/ֱm2m +@m +@mu +m +@ +@ +m"+X25 ;?225" +@5C +@51 +"5 +@"Q ++mz99"')Lw$95,I99AwQu999grT?99989925999/ '9990143232632#"326323276=4.43232632#"2#"&#"#*.54>7654654654&#""&#"32#"&#"#"54>7>=4654'.#"& H#"# JX&W8' B 0T  33 '66,n # '@/ /  R+%=f6'."!+c%3 A   $% $R  2P++* N9 IL+ 59+$39+(2+89++ :/3ְ.23 +@ +@ +3 +@3 +@3& +;+3!99&9 /98 9901432326323232#"&#"#"54>7>=46=4'.'& H#".  3B- / fT%  /9 I.,@1x0+3+0 + ++9+!+%2/ְ 2' ' +@'# +' +@ +@ +3+'9%-99!#901463232765<5'4.54'.'&43232632#"-7 / G ".{T(("ND5 <"u@#" <.<$ rD+f3>9+]2l+q"+l++#3"+ 22++/9+Ql +QO+/vֱY 2Yv +@Y +@Yc +vY +@v +@vo ++Yvi99qlBJc999>?9QLYr999/7z{$9 9014323263232>54.'&543232372+".'&'&#"3232#"&#"#"=4>7>=4&=4'.'& H&    +| " , hKc /KR%)a  0 3B$-  h6dw    W@S /JE  #     eo I.3GA)+,+&31"+1, +@1! ++ 3?9+2B/6ֱ 6 +@6 +@6. +6 6 +@ +C+6)991).9?#699 901432323263232>32#"&#"#"546327>=4654'.'"& 7FKq9&  V HI 0   ))'(H+X   Kk|r @ }+=C333w"+5G222+$39+'+3,9+/pֱL+22L +@2) +@28 +2/ R R/P3/ R/ +@RE ++6#+ f  #+ ff   +  +  +[2+  + + +gf+hf+jf+f+ #9f9h9g9j9 999@ hj fg..............@ hj fg..............@RpCHz}$9L9/ $@999w}8Ebd$9,Ta9999' !)$901432326322>7> 323232"#"&#"#"54>7654./4#"#".''&#"2#"&#"#".54>7654&'&+4 >%:$' " !"  3 :<    "  P6'A P|MN%  -H,   ,>d7  , ZUK+  18& :t(N"-% 0IdLgB+T+P9+]2++3f9+'2h/cִI+Ic +@IQ +cI +@c +@c\ +I +;$+; +@;4 + ; +@ ) +i+Ic LW$9 .EOT$9;D9PT?@99f87654&'&     T; !   $ 13 8& 06   " >uC\)"0  _&L,D_ -/  $F>VU,)"D ++ #/ֱ + $+ 99  99014632#".732>54&#",ʹVP6jqwQX2GmA"KJ:%nS2I^O"Uh(]cO3,?[5ɔR3+83/ +$ +Q9+3 +9+S/Nֱ' 'N +@'1 +N' +@N +'!+ ! +! +T+'N599! 399/3.1A999*C999$'KN$90143263232632#"&543232>54&#"6"&#"#"54>7>7654&54&#"- ~^$3?1 B 48$l_) !.t ; "'_Y1M,  ;'Cs%A;X    H>i5->:J%++% + ++H)@% +)"+)@ +@)/ +K/ִ; !+;- 2+2/-+-;E+ L+;2/9E-@ )579@H$9 %9)-2999@ 9H5 99901463232>32#".#"#"&5467654'&7327>54&#"-SM40eAG12.& " lB?., ([J'6FJ*SnS6OeT%Gs    #5ff@*,N]ۈf7E&SݔHY+1733P+-2+Vg++H9+"N +"g+Z/DֱY %2YD +@Y/ +DY +@D +@D9 +YS+ S +@  +[+YD49S1$999"*<>$9N9VA D999014;263232#"'&'&'&+"#"&#"#"54>54654&54.;2654&#"<Z'/'&62/'2w0= &/&P % UIab%[M3M" !/   N30> )-  (%=>g%'g1 N=>f'G;>9+ ++?/ֱ..++/+. +6%6 +!+@+9.9  &29$9%9+ !#6$99901743232654.'&5463232632#"'.'&#"#"&'.;  &,E)K[>P{'[g&i    6M>T1RZe!(cET, $ #3!O@/?-6]Yk!y$ 0<3-: 2#+@i{V8+>3+$M++ 3#5+L2$ +@$U +W/ֱH+( .2(H +@(5 +H( +@H@ +(+X+H >99(;9938999$8C999#901474>323272632#"'.+"#"&#"#"54>7676'4&#"#" X   ()    R I% #" 6FG   D(?  F -1++    X"   WJ+:+3 9+:+433X/Oֱ O +@ +O +@O +'+D$+D' +@D< +'D +@'2 +Y+O9' 7J999 *>DSV$9:0<$9014>23263232>54&54.'.543232632#"'.=4654&'& F !)#.>& $?%/ B6+  AA5ZF70  /(/ =#3$  +>`=ej#(0   -+]@3'^ 7)Dn6++#3+C9+-2"+"+E/ִ+ ++ + +F++09C6>$9 9 +999014323263232654.5463232632#"'&'.'.&C& -  9Tif(-   M&> %  88}= 68 EASB4 C+31+Ll333-"+HUg2221+,9+2/!ֱ^^F+U +FU +FJ +Ud+t +dt +di ++^!9?C$9UFVa$9d9t}9,@ =CFVadw$9-;91 /9Jt$901432326323267>54'."&#.54323263232654.5432;27323254.5463232632#".'.#" #".'&'.A'8 +8 8 x  B&2@0 ' &  z  (-  M_L ! s D#:A," 8<}>?(-|7    F:'  3DF    ֭:;N689M07a+Hg33]"+Kl22A+f3+$39+2+!"+,2/{ֱ3 鱈+3{UW999]A=Dj999.;MU$9! 99 9901463232632327>54.543232632#"&#"#"54654.'&#"32#"&#"".54>7>7>4.'&'".& _6Q  (!*8#("$/+\&!U- 3 ; S*6  K <   !*4P8  N{  9-1"  .3C>  0-V##oD   )/7 %H'v mO+U3F9+[22+3'9+l2n/aֱ> >a +@>L +a> +@aW +>"+4 +"4 +"* +o+>aR99":7>54'.543232323232#"&#"#"54>7654>54.'.#"'+ ;2* "I   "  A1  )   M#: *?2   &6b6"    0: U!&    0XR*'\,/1 DX@+++@ ++2 ++ E/ִ# +F+#!6=999+@9 &$9#90174>7664#"#"546547>32326323267>32#"#"#"(p+# /  7b9?M(+'eZ$   8W(M<"2#(*     2 N42^/+%+ 3/ְ2$+!22 +@ ++24+9%/+999 990145<&546;26232#"+"32632+"&5N+  N M Z RKh,L+  'F)OyJa2 )I+/+01432#"&'& 3 !    ?$4,[*+2+-/ֲ 222$$+2$ + +2.+$%9*%9!#9901430;254&45454'4+"&543632+"& \ Z d +B  'F)OJ`2 )=K'h,L+ A% 11!+L+! +@! +/22/+ +3+014767>32+".'."+" *)   K%G!!E&      "  %+ +/ + ++014>;2+"&  &  mP0?V3+<+C3+09+"+9+" + +W/ְ2@ 22@I+ 2+(2+I +@+1 +X+60+  RM +  RMRO +  + +  + + +   + O+  +RNRM+PRO+QRO+ROQRM+ #9 9NRM9 999PRO9@ O MNPQR...............@ O MNPQR...............@@9I"8<$9+6799C(68L$90174>7>7>54654&#"#".54632#"."#"'&732>=4#"#0=B/ BHi?+<?%)@N(%%  , U$1 #93< 'G)HS%+    <")!H   3A+&37P+++=8+2= +f+B/.ֱA 2A+ +/+A +@+ +@+( +A:+C+.+&99A#999:99=7+#$99929 9 9014>3>3232>32#".#"#"546546=4+"32654&#" , B1Yc1*:Q)F#  - OCEI_Q/!    'r$|S/{ ,@ C!=[P]CO|'(I&+M+& + ++}+ + +)/ֱ*+90174632#".'.#"32>32#"&'g#Q  K$cH#'Ns r'!?M=+13CM+++K"++#+C=+N/ֱ@@G+32$G$ +@G +$  /$ +@$ +@$, +O+G@=$9 9$/1999C=*6:999K%8$9"901746323254654.+"&=4>7>32#"54&54#"#"&7326574'&#"'rU  =     M*  9!]hH]I-JJ-AJa$L/ $ 2gD.   {L.!=,_' .~+!+ + ++,"+#  +#"+&2//ֱ !2)+ !+20+) $9  99 9# 90174632+"32632#"&732632654&#"'qZ*E& )eD*W (32#".#"327>32#"#"32#".#"#"54>54.54&#"#" 0GH + 6* !  (1 $   I{I(  *GI)    :N`   AS '1AS_ >+E"+ ++]"+#+N6> +N+W,> +W"+`/ֱT B2+3T+ + /3+TZ+) H)Z+9@+)"+a+B 993999T 9Z@ ,/67>EMQ$9H9)'9"9 9NE9B9996Q999, 399W /99]')$9 "9014>54.54>54.54>32327>32#"&#";2#".732654.+"/"32654&#"'$$ #I2'8+   `E/>H.vN-N?:J6>c& .- $)9+-080%4a% ,5$/3 *7  @X "X1:4/,-3( (b763232>32#"&#"#"546265464654&#"#'.#"#".547>554'&#"#'"' ? %A%QZ 6C AJ2/2  58    &Z}+%%`e    H9=Y'?l-7   #"U1$5@~+9+ +>/9+A/.ֱ. +@ +. +@. +@.& +..6+< +B+. !9>$9< 9&99 (9014>7>32#"&#"&#"#"54727676=4.'&74632#"&$    *(   9#& } " "  X" +) '*8&lD{k+Ge33o"+B2 ++{P++&+&+,+1"+Re, +"+|/uֱUX22u +@c +u +@um +!+. +.! +@.D +!. +!$ +}+u h999!:<=PRe$9.7GJ999okc9RPar9991!=99&$9{,9x90147>763232>7654&54323>322#"&#".'&#"#"&#"#"547>54654#"#1     -  ,]  & / [A7\$3$4 CL % /  %5Y5""*p     $o9 9+@  0   "1 K:}+$3"+*2 ++9"+;/.ֱ1522. +@ +@ +. +@. +@.' +<+. !999999  9014>7>3232#"&#"#"&54>7>54&=454.#" " '# K/     'E(8/A J%?E%'a+/5[333f9+(22+2^33"+V2 ++3wJ2  /"+/ֱ|2| +@| +|o+RNP22Ro +@RY +oR +@oc +R<+% %< +@%- +<% +@<7 ++| 999o a$9R^99<5J[$9%29a-7c$9f@ $9FNt$9w99 9014>7>3232>3232>322#"&#"#"54676=4&4&4.'&#"#"&#"#"5467>5<6454&4&54&#"#"&#"#"54>7654&54&&0   /%6#A$IO  !1 / *  12+ 2 N 60*B 74    ))V@;D -(  #"3+  9-U.+    /%5@,$& |R;`J+)3E"+%422J+T"+++< /_"+a/ZֱAAZ +@AH +ZA +@Z +@ZR +Z A8+""8 +@"' +8" +@81 +b+AZO9 M98/7>;232>7632#"&#"#"54>?6=4&#"2#"&#""54>76=4&+" ) -*(@$(2 ; L5+@ $  ++       %0( >1&  763232>32#".#"#"&#"#"&54>=46=4&#"&32654&#"4' !2Y^ub.9 -"4.)  //AEUH3(  #uT]n   +)vS7  #2 _EOx 7'5"@R=+EL+5+!+P+!+."+++N"+S/ֱAAK+32K +@ +@ +K +@K, +T+KA*8=$9 %999.!99=399E69N :8$90174>322632#"&"#"#"54>7>54654#"#"'&732>=4&#"'.JT,    ) ?'I0IF;F ,8Y./4"Af; @ %TCQV ) 2=.(=@Z"'(=&NiM1+73-"++++ + +L +L9+N/@ֱ! 2!@ +@!/ +@! +@@ +@@; +O+!@99-1=9E99LI99 9014>763232>32#"&"#"&#"#".547>54&4&54.#"&#"I  0)6/  : D   | () ,h$-  '@AT  7cC7+ P++%9+D/ִ+ (N+( +( + +3E+7>32;2>32#".#'"32632#"=4>54&#"#"  !  #    !53 E(    - &  /  A3 +u'pQ +F5+<+M+*22"++3+3EP+2G/>ֱ 2> +@> + +72'' +@ +H+ >99<9'59992/:99E #8>$9!99014>326323265'4&'&543263232632#"#"&#"#"54654&+" 0D+@:X  Q $C(  $a;XI6D2#  0454&'.543232632#32654.54;2#"#"#"&'&'.'. 8$   D ;  Us ! ;( ^z&"  $~;E   . %8"   !1UAcS1Z+}33T"+w2>+3D"+>+/ְ2t +t +@ty +t +HH3+A +A3 +@AX +3A +36 ++t$9  !$}$9H-jm9993dg99AD[^999TZdy999$-3Hgm$9D9>!6990174>7>7654.'.'.54323263232767654&543232632#"&#"+"54654.'&#"#"&#"#"%&%..&3 ! F. ) - $s "/  ( & "#0  " $, &*7)   Y  s  &9     (: (  AKY>++$3"+I22L/ִ + +@ +M+>@J$9>.999,90143232632+"32654&&543232632#*#"54>54&'.;  +Q1 1#&Y  -.r*   _  @  g=&& 8R"O(]JB+/!+/B +/8 ++ !+!+3+  +@  +K/6+:+L+:6!#=999/B99+99  9&990174>754#"##"54>56323263232632>7632+"+"#"(  c 0` *i/1 "$   r3  &) AG  7 *F2S+3 3+3/0ֲ-222$+0 + +"2 /4+ 99014654543232632#"#".#"#"54654&*% ( (I' "!& # ?J  22+?   C7N/!-"/!ִ$+2 $+22#+! 90145<&54632#"&5N   WKh,L+  'F)OtJa2&) F2Q+33+3+3/ ִ%$+22 % + +2 " 4+90147>54654.54#"&543232632#"&#"#" " 'I( ( %# &! ?+22  J?7C _ D/6+ + ++7+ + +/+ ++0174>3232>32#"&"#"4G#:A( +.-),.;5d4+(+/+6/ִ1 +*1+&+&/*+7+&$9*4$91  $9014>7>2#"'.4>767632#";        .g        "w gy'L_G+S +G+/d+/@+/@ +/3 ++Z9+Z +@ ++&G +`/ֱMMB+ U22>+(22>B +@>5 +a+BMGZ$9> 9S/,9&MW$9017467>264=4322#".'&#"32>32#"=4.".'.7325474&#"'SF      " #(  #>fr`+S+e+j"+S` +SX +bS`+M+%+2P+2% +2+ +q` /P+:C`% +3:2s/ְ2g+g"+55 +I@+I +@I> +I(+t+g995"@ ejnq$9 b99I:CEQ$9(%2;BS`$9bj9Sgn99qQ99C I99:92"9901646323>54'."#"#"54;254.54632#".'.#";2+"32>32#"'"#"732>7.#"!#%03 :xRBe 9 0C0 z  >M/G !8"Ru>&  (,# -0r +E bc<,"' 99@m  '<  %  ",&,&7   /8+7L+7 +@ +2 ++)/37+)3 +@)- +$29/ִ12+1 +@ + 215+2+5 +@" +2:+6-O+ +ұ+ & -O+ +ұ+ &+ +-O+ ++++ұ+  +'&+-O+ +*++ #99*+99 #99&9'9@ &'*+................@ &'*+................@0174?&547'&4632627632#"/"'#"&264&#"D'&C E45E E))E E43E hRrSS9:, E3DB3EE''E F8>=7FE'&DvQStUp+v3l9+i2+,39+'2v +O3Z2v +E3A22/ְ2` L2` +@`V +C2@`n +` +@ +2@x +`%+4 +%4 +%* ++`s9%?HO]p$947AEZ$9lp{99V97?$9 *499901463232632;2>7>54.5432323232#"#"326;2#"#"3232#"&#"#"54>7657654&+"&#"54;2654&54&'"&+"&54>3254/.#"'+ ;2*  G     A1  .Y pJ>  } Z  @ W * ?4 c  d [b0   &2a!"  %/ 1/    !'   1%$   ) ,.N/ =!/ְ2$+$+2 + /"+ $90146465432#"&4632#"&N     *g p+"&[D &;K[k+!P+>/Rl/ְD2\2+I+ $@+\U+e2;2+,2+m+I FH99\ J99$9U@ (35>RXbi$9799!R ;DFbi$999014>54.5463232632#".'&#"#"&'.543232654.732654'&#"; nR0M  (!"29;/YJJ+  |_-B-  %?\6Q^Q6<:\N0pa F 5 3!I_ )3@") +E- .  !Xf  ;6,&)HO0" $A#F,%8gfW, N +I/7P+7I +7= +//9+/ +@/+ +/O/ִ + +22 +2) +2;+?+?++P+;2 I$9/7 !$9014632#"&7 654&#"463232632#"'&#"327>32#".,ϕΗ*N}p&M- ;nK`)T9P8    '$&?_4 OӒϕf" k\K#CD*O :  "/?/ 9K<+&)3389+-21+ /"+  +  +L/ְ22+:22@+2#$+ 2M+9@ 48=4#"#>B '+  E+#. N;. ! 1 "#  + ,4#  J+( TA]&m(3/ + +/ ִ+  + ++01743!2#".=4#!"&     o ,|( Q Xh+9+,+/@N3334"+DJ22+ + /9+9_4 +9"+e4 +W3"+i/ִ + Q+=+Y2=Q +@=B +Q= +@QL +=b+!+!++j+Q IW$9=G9b@ %8$D$9!&)99,14999,41BGL$99 &8R$9_%$99e!U999014632#"&732654&#"74;2632;632#"'.'#"#"&#""54676=4654#"3232654&#"zVX||XVzjHJkkJHj@ ,%2    8$0  N ! "X{{XV{{VHiiHJjj& !  _  #87!q3E`T H+ //ִ++++ 9999014632#".732>54#";+02K,*!9&c#CG$/7&"=#I 5b /3/%323 +@3+ +3 +@ +6/.ְ2)+2). +@) +!2.) +@. +27+0164>3!2#!"'4;2=432;2+"#"&=4&+"&  U      O        */>()8 IQ 9L=+/3!+'2= +* +K+ +3M/ִE+E +EH +E+ 2+ 1+$$1 +$, +N+E=99 99189$4699=KE99A9 168$9014>767>3232>7>23232632#"54654#"#".#"#" '   %2"_H > & T5(2!F -^(U_2 $pA    H$ZHx#J$!,'6=,G4Ib+E"+_22/%9+% +% +//7"+c/ִ5 +5'+:>C222S+Ja22SY++Y +@ +d+5#999'%/999Y9/%OUY$9E7JM\$90146326326;2#"'&546323254654&54#".7326=4&54>54#"32>5454&#",~IV ( iV(  B#6D,0 c \v 2eew21%G. #J.; )/]dcst(! *X1oa^ M ./++ /ִ + + +014632#"&M"& M *) '+7^R?N L /"+/"+/ִ 2+ +2++  9999014632#"&732654&#"RH=Z]?FO;B*"/9418SI?AIPL=H204Q0'TT]0J/.3 + 21/ֱ + +2+(( + +22+0174654&54632#"&74654&54632#"&'_] *4004* ^] 3&65# fij %.) )-$ fk - !.. R'L@'Q?Ro'J@'Q?('L@'QA$7o+ + + +0/%+8/ֱ +2 +9+9!$06$9  %.$9 90174>54&543232632#"&4>2#"./.$,5,[@$?(6QH  #  i!TDX#  8$') ,<(     'Pq$'Qq$'Rq$'Sq$'Wq$'Xq$e+Wh+b333n9+2h+nh +@n] ++&鰣2+#8++ 9+& +@& +8+yb8 +8+-Mb8 +-M- +@MC +/ִ + + ++s2+ R2+ +@l ++A+;26+6/A ++ +@ ++9 hnv$9+Oe996(EMW$9A8>Cb$9 9nej$9W9y_99EAO999M>9998-+;99#999& 9 9901747676767654'&543232632#"54&'&+";267>32#"&'&#*&"#"32>32#"&#"#"5<>7>=4"&+"&#"#"&#"#";26=4.#"|TJ4b #Z/4+;(    L% )Y|6  W HU  ;2'  ##:7 [4  {  C ,-6q2   N"AW(H+X  Ep  +  S  7,7'^&H'P(H'Q(H'R(H'W(5'Pq,5'Qq,5'Rq,5'Wq,=t+3IP+2+3S:21) +j31b2u/4ְ#2_ >2_4 +@_f +4_ +@4 +@4 +_N+ v+_499NDn$9)I#91N9S 7990146;2632#"&#"#"54>7>54654&+"5432632654&54&'.;267654.#*;2#"+""W$EtH,,&r<*#< A  !  # #&D`,J?fv<!  j  *ONFgK/ * '.!  9#    '*GPM) # g ('Si1,)'P~2,)'Q~2,)'R~2,)'S~2,)'W~2X#/h /+  +@ % +2 +@ + 20/,ֱ, +@ +2, +@,' +21+6-T+ ) #Ҭ+ .+-T+ ) ) +##+ҿҿ+ +..+-Aҿ+ #"#+)*) +Ҭ+ .-.+*) 9 9"#99 #99-.99@ "#)*-.................@ "#)*-.................@01463232?632#"/&#""&54?64/&Xt } y|{ x x |fs } y| | xy |,)(6F\L +J J +@ ) +&++DD +@ ++]/ֱ7 7P+ ^+69+ - '- - +''+&'+9+ -.- +>- +?- +'W'+X'+Y'+['+.- #9>9?9 9['9Y9W9X9@ '-.>?Y[WX............@ &'-.>?Y[WX..............@P7 +3G$9J 09D354.#",ʹ!6     E[jq!8  ,.EIX%(  _(nZ-"KJ:%*-3^XS> B se,U] b 1.^F9 . ,?[5CuF3*m˻ 'P8'Q8'R8'W8'QR<\>+D3:g++[9++ &> +9+.> +P+2]/Vֱ 12V +@ +@< +V +@V +@VF +++ + ++! +^+VA99+:>$9:>9L99M9&!9.#2P$9[ 99014323263232632#"&543232>54&#""6#"&#"#"54>767654>=4'.'& H#".F^$3@1 B  58$m_  t < /;`Y1M,  ;'Cs(CF,8  &M] .,ph+&o33d"+@22$+B"++T"+q/ְ 2aZ]22a +@af +a +@ +@ +a1+K29+$+9Q+@+E+$+r+al91j99+T99I99QHN999$B99E9Bh9d,+99T 8E5$90174>54&54654632#".'.=463232654.54>54&#"#".#"#"!!oY)+&-&@L@/$     .*<<*!"-!" @$  3%)J,59+J, ' W     ''/&);"0,.@$15 "8cA2GG=F(  0'PD0'QD0'RD0'SD0'WD0'XD0RbpD+9!+O+V9D +9< ++)39+n2 + +e3D +e"+h2q/ְ2S 22S +\2c 62ck+/ !+>2r+S9 O999c#%IK$9k39D)$9/1<9939IKS_$9e/9 #%ck$90174>767>54.#"#".5463232>32+"32632#"."#"'&732>=4#732632654&#"0"+J"+L"+4( #959 9>"9L9J999@ 45>JL (...........@"45>JL )..............@/-98$,A$9H99D 99A,9;2H$990174632327>32#".#""&54654'.727>54&#"32654.#"'}o,  ia,' 46O!44Qg49Ng&\ 1 599YS$AB$999IE9999  9014>7>3232>32#".#"#"&#"#"54>=46=454.#"32654&#" " $G,Y_ub.9 - "4.)# //ADTH4(   'E(8g ##uT]n   +)vD+%  #2 _EOx 6A'W\h'Tq$0`'TD~'Uq$0v'UD4[h+TZ339+4+.+&&. +&) + +f +@`Z +@L+i/ִL +L +LQ +L1+#N+#6+ +j+L91B\f$9#9767>32#"&#"32632#"&54>54&'4'&'"#""#"&#"#";7254&#"+RiX    * @ **;-*")"3B!U   97 !_'b ` I6O"# .#+ ,;#6) 9# V  04UlR+Y1+I+@+9g+9@ +9< +"+9+" + +m/ְ2V 22V_+ 2+(26+_+C2+C/62+6C +6= +G+_+1 n+60+  hc +  hche +  + +  + + +   + O+  +hdhc+fhe+ghe+heghc+ #9 9dhc9 999fhe9@ e cdfgh...............@ e cdfgh...............@V9C"RY$9_N96LM99GJ91+&399R96C99Y09(/LNb$90174>7>7>54654&#"#".546323262#"&546?4'."#"'&732>=4#"#0=B/ BHi?+<& M 9%(: )@N(%%  , U$1 #93< 'G)HS%+  7$ ,2#$4   <")!H  ,'Q&''QF,'R&''RF,'V&''VF,'Z&''ZF'ZR''v'\ G'!ZhX+L3^M+,++f"+>+^X+ f, +43 92i/ֱ[[b+N2?12b? +@b +@b# +?/3? +@?7 +@?. +j+b[ X$9'9?*JL999^XEQU999f@S$9 =9,19017463232=4.+"&54>326;2654&+"&=4>7>32;2+"#"54&54#"#"&7326574'&#"'rU Z ! =    -   M*  9!]hH]I-JJ-AJa$H 2.   gD.   {L.!=,_Hh'T('`'THH~'U('v'UHH'V(''VH4HW~+L鲁+9+^+p+hhp +hk +++ 39+ +@ +-+"B- +"B" +@B8 +/ֱ DG22 +@ +@ +s+eN+es +@eU +em +e6+02+++/6++ +@ ++~9s=BL|$9e:{99+y96-38999^9 9hs9~9BL3SU$9"9- 09999 99014323232632#"54&'&+";267>32#"&'&#*&"#"32>3232632#"&54>?4#.#"#"5<>7>=4654'.'&; "Z04*<(   K% )Kq9&   " # 2%,0  8T 0 C +.H_2   N"AW(H)  %'&7#"  Fp|r @ '4;I3+936+3 +@ ++)+!!) +!$ ++G"+> 9 +>"+A2J/ֱ <2-+2+D+ !+2K+- >G$919999DA9  !&)$93!-99 99> 90174632+"3263232632#"'&546?4+#"&732632654&#"'qZ*E& )eD*W [ /%-  X{T 8406d+0^l- L . & 7- lPOH'Z(''ZH,'R*'1'RJ,~'U*'1v'UJ,'V*'1'VJ,7']*'1'[J'Rc+7'RK+OU333{"+2+J"+&+ 333)"+ 222j + +:33222/ֱ22u22 +@ +@~ ++a22/ >DH222/ +@/6 +@/L +@/( +/ +@Z ++99 U$9/#R99JZ~999j{]H999A99>99969&) 9990143232632#"326232654.43232632#";2+"2#"&#"#*.54>7654654654&#""&#"32#"&#"#"54>7>=4654+"54;25&'.#"&;22>54=4&#"#"&#* H#"# `a ' B 0  + ) T  33  EE,n # '@/  ; < / 6 -j :v  O-'.   Ic%3 A   $% $R  2P++* N9 @ &  0 07y=+Cah333:9+=+]"++2+R%R +3%v2z/ְq2(,22 +@" +@ +XX +@X_ +X +@ +2@l +O+55O +@5< +O5 +@OE +{+Xe99O%.2CR]$95@9]=E_n999:G\H999R,5.Jo$9209%)99014>;254&#"#'"54>7632;2+"32>32#"&#"#"546265464654&#"#'.#"#".547>554&+"& +  ' ?  %A%QZ 6C AJ2/2  58   )- ="   ;1  1A%%`e    H9=Y'?l-7   #"UP  5'Sq,'SX5h'Tq,`'TX5~'Uq, v'UX45R+:=339+A20+(g+(0 +(+ ++Q9++ S/LְG2L +@ +@ +L +@L +@L? +L3 %T+L3;E99%6899099(%399?9 H9Q 9901432326323232#".#"32632#"&54>54##"54>7>=46=4'.'& H#".  .&. - / fT% B,  6#, /9 I.,$4MX+79339+-+%g+%- +%( + +V/Q+Y/FֱF +@ +F +@F +@F> +"F+02+0/"2+FFN+T +Z+N099 @9014>7>32#".#"32632#"&54>54+"#"54727676=4.'&74632#"&$  !.%- (   9#& } " " ',  :#*  X" +) '*5'Vq,$3Z+9+ +4/,ֱ, +@ +, +@, +@,$ +5+, 999$9 &9014>7>32#"&#"&#"#"54727676=4.'&$  *(   } " "   X"@W'-H,$8'ML@'Rn-8'RgD7']L.7']NG'Q}/'Q]*O7G'] /7']aOG'\p /C'\ OG'y:/'yOGd7<+-?+93D"+D? +@D4 ++ 3b9+2e/IְZ2) &22)I +@) +I) +@I +@IU +f+6Ĥ+ WP #WW+W+W+W+P$P#+OP#+WXW+XW #99999$P#9O9@ #$OPWX...........@ #$OPWX...........@)I<99D<A9b-6L999 901432323263232>?63232>32#"&#"#"546327>=4+##"&54?654654'.'"& 7F Kq9&  V HI . < 0   ))RzW  `(H+X   Kk6   r @ Z=+C38"+I2++"+*+[/Mֲ PT2224!24M +@4 +@4; +M4 +@M +2@MF +\+4M@999*8%'999 !"999990147>7>5654.#"54>7>323263232#"&#"#"&54>7>54&=4654"#"&    " . 0 '# K/  +P   I%  'E(89    J O'Qi1;'Q Q7']w17;']Q'Zi1;'Z Q8%'Qv,)h'T~2'`'TR,)~'U~2'v'UR,)'Y~2''YR,`zZ+L]+gZL +@ZW ++ +8++x +@ +++ @]+ + @ +@@6 +{/ֱa am+H A2 B2H4+.2)+)/4++ +@ +|+ma]x999HZ9)8@L$94+16W$9 9@L1RTm$9 n99+.a999qs999x 901463232632#"54&'&+";267>32#"&'&#*&"#"32>32#"&#"#".732>76=465<.'&#",ʹ #Z04+<(    L% )Y|6  W H/y-wQX2GmA%,  ;AnSC ,-6q2   N"AWH+X h(]cO3 $|r 2*'4:FT*+!+8+>9+>8 +@>" ++3D"+R2I* +I"+L2U/ֱ; ;A+ G2O+ !+$2V+A;899 02$9O*$9"99$02;A$9I9D GO$9017463237>7632+"32632#"'.##"&732654&#"32632654&#"'}orC-;*E& )eD*X (57i']U'Z5i'ZU;'Q 67c'QV;'R 67c'RV;7'^677c'^V;'Z 67c'ZV7'^977F'^W'Z;7F'\WwG+M3+$M++ 3#5+m2$ +@$v +e[G +93e02x/ֱW+h2< '2323272632#"'.+";2"#"#"&#"#"54>767674&+'"#"54326326='4&#"#" X   ()   $]    R I% #" DE#S6FG   D(?  F -1M  +    = $   FbW+Od+OW +OR +$+ M+.522$+6$6 +@$ +)+.+aW +>3aF2c/Yֲ ]222L9;22LY +@LB +YL +@Y +d+LY9aO]9B9  999$6990174;25457654&#"#"54>7>32;2>32#".#'";2#"#"32632#"=4654&+"      !  #     v ?) !53 E( "   - &  /  D   !A3 +u - 'S8+'SXh'T8+`'TX~'U8+v'UX'X8+'XX'Y8+'YX4qb+K+Y+Qg+QY +QT +:+3 9+:+433r/iֱ i +@ +i +@i +\+NN'+D$+D' +@D< +'D +@'2 +s+i9\ 99N`b999'7JVY$9bQN\99 *>Dmp$9:0<$9014>23263232>54&54.'.54323263232632#"&546?4".'.=4654&'& F !)#.>& $?%/ B6+  AA) /&. 7=D70  /(/ =#3$  +>`=ej#(0   -+]",  6#-  3'^ 7)4+a W+M+*229+O+I+Ag+AI +AD ++3+3`P+2b/Yֱ 2Y +@Y + +P2'' +@ +>'+L2+L/>2+>+> +@>/ +c+ Y99LW99SU999'>;AI$9WA>L992/PU999` #STY$9!99014>326323265'4&'&543263232632#"32632#"&54>54."#"54654&+" 0D+@:X     .%-$C(  $a;XI6D2#  054.54&547>54>32#".#"32#".#"#"& 0GH   (0 /   2|e I{I(  0T8! MG"+*M+*" +*' ++/362F/F + +H/ֱ+>2+?2> +@ +I+6?z+ . ?A  + + + +A@A?+  #9 9 99@A?9@  ?@A......... @A.......@F99>99<9499*901463232>74#"#"&546332767632#"&#"32632#"&#"#"+  ( #  2?9E/ 2!B 93  8%$ 70/G   [P$+)  ͹+4 ;7']677c']V7']977F']W8%[!+f+! + ++!+"+&/ֱ +@ +'+99990143232654.#"54>7632#"54& (  /, ?1'+C^0    "7bR[J\kMRkMZjWvUV'XH nSpYK' / +/ֱ + +014632#"'&*   b| "8  g% / +/+  + +014>7632#"' )a H6 f+/ + + +2/+ +0147>;2#"&#"#"  , ]Y -  C&UT @/4+ + ++4+ + +/+ +01463232632#"&#"#"CL) BO Y08 T03`(//+ +990143232#"&#"MC%   M v+/3+ +@ + 2/+ +014;232>;2"& +$* ?p>s##(VSW */++ /ִ + +014632#"&#' N +) '* 7/3+ 2+/ִ + + +014632#"64632#"## A%# %'%# %p H /}+/}+/ִ + ++  9999014632#".732>54#". %%9% &%O36$)! )Q /3 +2/ִ ++ / +@ + 9 9014>7632#"74>7632#"' )a ' )a H6 f H6 f+/+ + +2/+ +0143232632+"'. \Z   ,UT-  C&W-/ +/ִ $+ +  + +014632#".?  1+4    JY- / +/ִ $+ + + +0147>32#"54>54. @   +5  7Y\7Y\,|( ,|(   /L+L+ /+015463!2#!"& ,    /L+L+ /+015463!2#!"& G    /L+L+ /+015463!2#!"& S  sI0++/ֱ  +  +  ++014>32#".I'!   *H' &% 780++/ֱ  + + ++0147>32#"&54>54.8  &! (*H' &%8./+/ֱ  + + ++01747>32#"&54>54.8  &! (*H( &%I0++/ֱ + + ++014>32#".I    $)Y( %-)EI&ou8&pv8&qwJ9R+!3+52:/ֱ + + ++-- +-' +-2 +;+014>32#".74>32#".J   !'   !'Y%7    & 'H*%7    & 'H?)O+N+3+2JN+"3"+2%+G+P/ֱ9H22+$222B+ 3* +2Q+B;J9999*"99JN$H999999 99014323254.543232>32#".#"+"&54.54>54#"#"?+N- <=$P5&P5      ,M-=$M54Q%<=)@$  $Khk ^eO+  #D,;3lh+X39+L2hk+T3+P2`+(++83+42+<3 "+02+?+m/fֲ"222Z+.>J222ZZ*+& +&/ b33* +D2^ +n+f& h$9Z(`99*,099 699".99(&,990174323254.54>54#"#"54323254.543232>32#".#"32>32#'.#"#"57>54#"#";;R  #M5-O* <=%P4-Q-  'P3O'@?  ?=rF +R4<=+N-.Q,<=2P-*8S,@?  -M+S' /;h./++/ִ + + +01462";X|YX| |XX|Y6''T]+/+/ֱ   + +2+0164767632#"'&')23 ]] 32) #+, gg -,#'T]+/ +/ֱ + +2+0174654&54632#"&'^] 3&)23 ffh - !#+, sh/ִ ++014>74>32#"Ovy  ޑH|   O( !2\)///302/32/3/1ִ$+4+1.99/%'99 990173>32.#"!!!!32>?3#".'#735B_Z*:Z-"O.;rU;v:$?,K]#FH<R7#/F\ $*2 $FM/ 0HK ==*O50' //+0174>3!2#!". U   |+MQ33"+C2|+Y9++"2+" +" ++++)$3b522/+/ְ 2l鰐2l +@ls +l +@ +@~ +l_+[22<'2<_ +@<1 +@32326546763232>32#"&#";2>32#"#"2#".#"#"54>54>54&#"#"#".#"#"54>3654&454&+"73232654>54&#"  6/CO2# +u+Mk]i99D 32#".'&#";2632#"&#"#"54>=4&#'"#".#"#"54>546454&#"&#"&!/LU+#  +&"  K   46" &$, @  JvE$ :  (mA %f/-  +;Hc <]//(  BS  u]+Y"+2g22'+-d33++:f++A3tF+K4+q2v/kֱ o22S?N22Sk +@SH +@S[ +kS +@k +@kf +S5+5 +@$ +5 +@50 +w+Ska95-:_$9*999Y'$9t"5Nho$9:F8?$99901463232654>3232632#"&#"#"&54>54654#"3272632+"#".#"#"5467654&454#"#"0HJ"   F' I!#D"Z  % N#  IyE&$B(8/A "   ZG;" 'Ie"'  ']222+7b333"++M+ + + +++ 33yP+J22y +@' +)+/ְ2鰞2 +@ + +@ +@ +u+2U2Uu +@U_ +uU +@uj +UC+--C +@-2 +C- +@C< ++9u h$9Ue99C $:Jb$9-'7$9lh99?o99yBXp9999 990146;26546322>32#".'&#";2632632#"&#"#"547676=4654&#"#"#"#"&#"#"54?>54.#"#"#".#"#"54>54&+7;254>54&#"  ^5&C$M %%x)&M )  ,6  .!= )  (!X <(0 *  ) OT FAR{ r!""5  52#$   c(+^+-%    uG8$   4 4O(0db+/333^9+2b+9"+++Ad+ +++H33{g+SVx~$2{ +N +/ְ2鰤2 +@ + +@ +@ +p+2ZF2Zp +@ZP +@Za +pZ +@pk +Z?+? +@, +? +@?7 ++9p i~$9Zf99? 5d$929999b,`k$9^mn999{:<&Zu$9#99 ?F$9A 999 90146;265463232>763232632#"&#"#"54>54&54#"32632632#"&#"#".#"#"54>=4&54&#"#"#"#"&"#"#"5472>54+"7;2>54>54&#"  f&L?5   '  J(D"!5 # +! .  &/ @ = , A*2 (   **zL"G  g(1$A(84&o     Z0A ' _  U(t%u0 88   8 1Q-,E';wu+;3kd+32ku +k6 +ko ++*F2+a* +# +M+U+Q/9+x/ִfN+f +fq +f + 2T+T +@TY +T=+A200= +@0% +0 O+O/+2O +@OK +y+ fa99=TFMQ^$9*kAIY[^f$9aK9W9Q N99901746763225'&54>32;2632#&#"32632#"=4654.#"#"54>4&#"#".'&#"32>32#"&'=.5H>./7" I2!62 C( 00-"5K3W ))J/";" 3Y(^q6e#) ' 2  +C4 +u*  %40,&$ # , D0@<' &&p8mR+[+_"+M2++.+8f+8. +85 + +L+ + ++3D9+Ahk222#+3n/ֱae22FHK222 +@P + +@ +@] +<+*o+Vc99< .3T$9*#999_R]9D*)99&99014323254>32#".'&#";2632#"54&543232>54.#"&#"#".#"#".54>546454&#"&#"&!/LU+#  +&"." 32#".#";2632632#"54&5432326546=4&#"#"#"&#"#"54>=4.#"#"#".#"#"54>54&+73263254>54&#"  ^5&C$M +x)&M ) ?1' ' A!W& (!X <(0 2  )_) FAR{ r!""1 97bR+K~g'v'(+^+-%  "10  $   4 4O(0{(*+13P+b+i3b+^"+l2+ +>f++E3wg+P2"+8+w +K +/pֱt22XCS22Xp +@XM +@X` +pX +@p +@pk +X<+|26 6/6 +@63 ++%鱋+Xpf96 >d999<19.999"*999w%69.Um$9999>"323263232>32#".#"#"54654654654#"32632632+"#".#"#"5467654&454&#"#"32654&#"0HJ" ?+`a1*:Q)F#  I!# %Z  % N#  OCEI_Q/!  IyE& !;\~Q/{ ,@ C!=+G;" 'Ie"'  '76323263232>32#".#"#"546546=4&54#"32632632#"&#"#".#"#"54>=4&54&+"#"#"&"#"#"5472>54+"&7;2654>54&#"32654&#"  f&L?5   B1Yc1*:Q)F#  D"!5 # +! .  &/ @ b , A*2 '    L"G OCEI`Q/!  g(1"$|S/{ ,@ C!=H#0A ' _  U(s%u0 88   3 1R.,EEP]CO|La.+(LR$3H"+2.+%9++ +df++k3g+v2+= +q +\+/ֱ22~iy22~ +@~s +@~ +~ +@ +@ +~\+B2\ +@J +\ +@\V +\:+!!: +@!' +:! +@:0 ++~9\R 999O9999:.=HM$9!+9H.0JX$9%2G3$9@ !5:Y[{$9=99di9 90146323254>323263232>32#"&#"#"546265464654&#"#'&#"#".547>54654&54#"32632632+"#".#"#"5467654&454&#"#"0HJ" %@%QZ5C AJ2/2  439  I!# %Z  % N#   IyE&%%`e&   H9=Y'>m.6   #"U  XG;" 'Ie"'  '76323263232>32#"&#"#"546265464654&#"#'.#"#".547>554'&#"32632632#"&#"#".#"#"54>=4&54&#"#"#"#"&"#"#".5472>54+"7;2>54>54&#"  f&L?5    %A%QZ 5B AJ2$P 58  B"!5( +! .  &/ @ = , B*2 #   **zL"G  g(1/%%`e    H9=Y1#o.6   #"$M0A ( _  U(t%u0 88   8 1Q-,E,}+=A^d$3"+92>+3+ +tf+(++%{33}+2+4+(+-"+K> +K"+/ֱ22y22 +@ +@ + +@ +@ +n+N2n +@nf +nn +@\ + +* +* +@*; + * + # ++9nd t$9a999T99 34EKW^$9*->A999>\f999KEZhn$9 34$9-9#9toy99 90146;2654>323263232>7654&54326322#"&#".'&#"#"&#"#"5467654654654#"3272632+"23#".#"#"5467654&454#"#" 0HI"  - ( ' T XE7\% :B "/G!#D"Z % N#   IxF%+*p    @ l< "+$  0  *  A;" 'Ie"-+  'NQ999xrj$9YW$9%+=D$989.93$9 999 90146;265463232>76323263232>7654&54326322#"&#".'&#"#"&#"#"54>32676554&#"32632632#"&#"#".#"#"54>=4&54&#"#"#"#"&"#"#"5472>54+"7;2>54>54&#"  f&L?5    - )f & / [A7\$3$4 %L " *"!5 # +! .  &/ @ = , A*2 $  **zL"G  g(1+mT""*p     $o9 9+@ !    Vi[,A ' _  U(t%u0 88   8 1Q-,E x+T7+%+7+2"+%7 +@%( +"27%+g+ "+aix7 +aL+ai +@ad +=x7 +"+鴟x7 +/ִ+ +@ ++}2+ +@N +F +Q +++^+l+^l +^f +l+:$++99 9F$9Q@K$9x9=99^+.4T$9l!(7999aTQl99if}99=FKN$9999%:$9 +$99.9015467>232>32327>54&/"&#"#"&54654&+"54632#"&#"32632327>7>54&#"#"54632#".5463235>7>4.'.54&#";2#"&732654&#" A<- >+1 + "  C8#D) Q ( !  d6Ym +U#!''  6%/.V<-K ,#8;  0?QX5)-*0DS!+=3%"+92!+9+!+ +-J! +-g+T/ִ7 +7 +7; +U+79%!999-(799J99  Q9990174>7>767632#"&#"#"5432654&'&#*"#"#"&#"#"73263254/&#" a   V=8H D %1?  ''67  Z#> '02 Ł   Y4   $"F9FV+>9+(2!++3SP+72JC +J"+W/2ֱG:22G +@2 +@2& +GN++A+ N  X+G2!99N>C$9A9>!&9C/99J99S 9014332632#"&#"#"54>7>4546=4.#"&32654#"7;2654.#"P T 76 (L2Z2    %*.=&<% 4%" / &  / ' '. Lp=%m.r $) #"'2d.+!L+!. +!& ++9+3/ֱ  +( ++4+9! 999 99017463232632#"'.#"32>32#".'p<)  !\34$,%M2)B!!-H)Ed3c . 1 DE"EC;H3 # ,HK(/?v+32P+2+ +3399+)2@/"ֱ0"0 +@" +@" +06+ A+0"996 9992 9901430232632#"&#"#"54>765464654'"&".327654&+".#sy$$W0q= /  Gw.7{a$  og;P*#  Xn SH-5KexbQ+D9+T+X"+QD +@QN ++ 3+a9+ +@ +%;N +%P+;% +@;5 +%; +@%, +c/[ֱ@=22[@ +@[ +@[V +@(+.+32.++d+@[Q9(%7D$9.015N$9 9XQV9DY9;3IK[$9%0199.^999a9 99014323232632#".'&#"32674>32#"&'&+"32>32#"&#"#"54>54654&'" *c k/ >%  $#6P 8&n2@,  + ) 6/3 7#H  c" ^K+Q3H"+K++}++^9+ +@ +$@K +$9+@$ +@@6 +$@ +@$, +_/Zֱ!B2!Z +@!I +Z! +@Z +@ZS +!9+4+.24+ +`+!ZN99'K9994,99HKS9@3UW999$19 .Z999^\9 990143232632#"'.'&+"326327>32#"./.#"#"&#"#"54>54654. nG-J &  !    "$! S 0  ,   68 NA   O)(<:++9+'+: +'"+=/ֱ $+44$ +@41 +$4 +@$) +4$++/+>+$+:$9,999 9'4799+19 $9 9017463232632"'.#"327>54&#"54;2632#"&o)h^5?$&cE5*) ]* --d~e#  , >?"%FCXx '  C H|Ah+I3c"+?N22l+F3q9+0+ *33369+ &~222Xl +d+/yֱ]2y +@ +@f +y +@y +@yo +T+2<P99X`t9969vy$90 (-$901463232632326;2765'4=4'.5432326322"&+"54>7>=4&#"#"32+"#"&543265464654'.#"./,7 &6G"?C  !3 Q F  g>%,Z7   )   "$- &    #IZ:%:%)X M " *I9N6p+$3"++ 339+27/,ֱ, +@ +@ +, +@, +@,' +8+,!99)99390146323263232#"&#"#"&547>54654'&#"./,7  #I .  1    6  ,EBl";5u3+6+3 + ++9++'"+6/ ְ2.. +@.$ + . +@  +7+. 9+.99'9014763232>54&54&54'&#".543232632#"&   5 0,7 H#  (!B3*CBl& * mF,LAr_+Y"+e+=3i9+e+e+'+"33,P++!&(333q"+2s/mֱV2m +@ +@] +m +@m +@mg +t+mb99Y_:g99i89,2NPVm$9q9' )$9014323263232>54&54332632#"&#"#"./.#";2#"&#"#"5467>54'&#"J%`a5 "#-'F-J^2 & 1)  *  .4 2  5   < Q P]   >+^k   7 W   YD!7#+9+&+*"+&* +@& +++79+8/3ֱ3 +@ +3 +@3 +@3( +9+3#99*#(9+97 -$9 9014323263232>32#"&#"#"54>54&54654. 9' &7M 57n&5#  (_ =  .-! ^%++@MR$3!"+.2+33^9+_/[ִH+H[ +@HL +HY+Y/YH +@Y +@YT +H+`+H[OP\999(M$9!%>KT999^@  9=BEHW$9999014303232767>3232#"&#"#"4>54./4#"#"/&#"#"&"#"54>7>4&"e$18 !#> 0 /%  T# && )9  <Fbp% <\;  .41X9G<AE<)!   .3;c|>+P+M"+Z2P+V3@++%+33b9+022d/`ִH+H` +@HN +H+5+8;225 +@5. +5 +@# +e+6Ϸ+ @.D  + + + + + + +DAD@+BD@+CD@+  #9 999999BD@9C9A9@ AD BC.............@@AD BC..............@H`S99 %P9995(9MPX9b18999 9014323263232=4.'.543232632#"/.'&#"#"&#"#"54>767654#" /  |   Q&   W(|&#!$5 T   ( ! $,)   #k i0%>)   <3n'# G ++P+/ֱ + +  99990174632#"&732>54&#"'k\heUrH+? uBKXtwfR",?'V{^P9+.9+D2 +3#9+O29 +"+Q/LְI2((L +@(7 +L( +@L +@LA +( + +  +R+(L<99  999.+9I99# (L999014>3232632#"&543232>54&+"2#"&#"#"54>767654654&#" Wq C\ *B  '>6  Z4  *>=!5*#B Z'&  =A"$'?8C%+<"++++A9+D/0ִ(+92( /(>+ E+(069>"%4$999-9%"+0$9A<4999017463232632#".'&#"#"&546?4.73254&#"'ke '' -U!&$ 6'#&0 %: $SrFqJIWfsk*I2(6   %m<' 92)RG]SZPb@+E"+42 +@+"++33_9+O2T)  +T"+c/MֱQ,2QM +@Q8 +MQ +@M +@MB +QZ+ d+QM=9Z$:$9 99EF99)$1999T9_ Z999014323263232632#"'.'"'#"#"&#"#"54>7676=4#"3232654.#" :XJb"!'  A5 / aC  :  <- 5$ =B+  <4 f[ !G  #04n($1 !'f<5+ P++)"+=/ְ2,2+, +@, ++, +22+>+,9  )/5$92%99) #2$99901643232654.546322632#".'&#"#".'&'&'  C#.05LL5UD.  (-"05KK5gE   "3.")9*:F +8.&,% :*GL\;+53@9+12 +3O2O +@O[ +@O +]/ֱG+K2++G +@+3 +G+ +@G= +^+G;99+ 899@;39O"999 9014763232>2#".'"&#"#"&#"#"546762>76=4&5'4&+"#"ZFD^    )#  T K    &. _6!;  )0       )F?n   AM?++,2333L9+ 722N/EֱE +@ +E +@E ++:+: +@:4 +: +@* +O+E9,?999:/9L:E999 %499901432326323267654654.'.543232632#".546=4&'&?1 A1WL ,/  .=0S%H4 <u-4z.++"+35/ִ + +  + +$ +$ +@ +6+ 19+0999$*99.+9"*099901462;23254&5463232632#".'. /"F,(  / sN 5+  # <_#7 \ #n   EBVoi+U3+72E++330g+E+K"+p/ִ + +  + :+H +:H +:= +q+ l9:Rk99HP9iR[e9990 :OP`kl$9EK&.=$901462;232>7654/.5462;2254&543232632#"./&#"#".'. ."D.  3   &2 ("G,( /wL9&  "H"$   &5*>_)3   *m%$: F  \ 7m  GMI  RB:gQ? "8j?+Q3C"+L[228+W3?+49+?+ +3$9+2+3ig++k/cֱ**+" +" +@"6 +" + +l+*cG99(=AD$9"'8;999C8694\9i &DJ`$9$ "999014323263232654.543232632#"&#""#"54654&#"#"&#"#"54>7>7654'&'.#.W*?] 79: 9,>!$  G+,fbT5 %2 4Z2 %  %Xq  ;-I)$   w    %8 6m=ZA+G3<"+O2+3Xg+2 +"33 9+[/Sֱ77S +@7? +S7 +@SJ +7+( +( + +\+7S D999.99(*7654.5432326322#"&#"#"&54>27>=4'.#"&.F &D,   ()8M J ?   !d  p/,   ;_    24*G. 'z42+%9+2% +@2/ +%2 +@%* ++3 }+  +@  +5/ְ2 +# !+# +#, + + +6+ 99#999 9%29 #,$9 90174>754+"#"&546546323263232>32#"&+"' 1*2 -!0e"03I ;!S o &6)! t  O ~YkT+]L+Df+G2 +9++P&T +P+2.T +2"+2l/ִZ2+Z +"N+"(+A+A( +@A9 +Aa "+a/03(m+" gi$9a RT]c$9(&.3P$9A49D]I9&Aac999.;>Z$920999 "gi$90174>?4&546322632#".'&#"3254.'.546;263232632#".#"#".732>54.'.#" G0N   '4"{  < ( \)@#'="G/-H7):X#.^+  %0)!,1& ''f>  &(  $GT;q  K-& +@h`v\+ 9+y2 \ +  +K+7/nq33/ִa2+aZ+ 22O+w22OW+W/WO +@W +O< i+i/3<+92"+"i +@"* +O+G$++a99W \999Zdq999ign99OU99"<Jy999%-7?@$97 *-Ggo$90174>23254654'&5467654&54632#".'.#"#"54654#"&'&23254654#'"#"32>54.#"@   B $`E  !6  % /# gJ ;[ P65 !-##'_"&-@9 ,F`   (3  Tm "'8"Mr   * F '15,{* /?*2 5* O++2+/ִ2++$++9999901462#".732654.#"*kvl1M0 <+V6EL'T9JJCƘ&>Y^yXovVg2+#9+2 +/ +3/(ֱ( +@ +@ +& &/& +@&! +( ( +@( +4+(99 9/#99  ,990147>7>322+"#"54;256=4&#"#".6 ##!vQ  3 0c )~; %) (U B;u6+"+"6 +"+ ++ +  +  +7654&#"#"54>323:6267>32#"&#"#"&B:r3,9/!8 )A%=^L\LJ13"+ # AV [  BobX*6# JC>n  0 <<<o:+g+: + +(+3+( +# +=/ ֱ7 +7 + /+ + +% +>+ 39+79901743232>54&#"#"54>7654&#"#"54632#"&<6'.I(AL   ?5':(r4;V)@g8d/+BA?T 1E'8 $J<9";3Rw,*HW +%/A3+K2% +@%6 +% +@ +X/ֱI I?+O2'2'9 !+9/?S  Y+IE99?9U999S69' 99%9 IOU$901746>3232>32#"&#"#"&54>54+"#".7;2654>54#"*D   ,   I X d    ( S#/" "6b?E ZpADB+P+2++8  +B2+)/+) +@ +E/ִ0$+0 +0 +0+= F+09 8B$99=992 9)80999901743232654&#"#"54>7636767>32#"&#"32>32#"&A>/TjLJ&0    % 2#=  -P4/LY./]~YAW  H:  Lj!:HFrE$//f+%++ +0/ֱ (+1+(999 999+%$99 90174>3232>32#"'&732654&#"/EdsW$W.AKC.MhzXC.nS#<%97654&#"&#"#"&#"#"MQM5 5A5 0Gf   X 3) 2U  051! "6E%@ "3&5D$+*g++Bg+;+3+E/ִ'2+ '+6'>+2+- F+>6$*3$93*99;$9B 990174>54.54632#"&732654&'.#"32654.#"3&%$qO3J%##**-Z=Zw9hC8L(] G&*:+ "<=,1I)E( ='Vd33':)F*#EF+j}XiQK8"*U%8 @=7189)n +"++'!  +*/ֱ$+ ++ 999$$99!9'$9014632#"54767>54#"#"&732654&#"9v^a|_Y7x#aa!9"9\fJU=A>TD<=eqgD*0 d#wS\FNV$L7A'] 7']7;']7']7']'7f']7']'7'^'7f'^7'^0'P)'P)'Pg)'#'P)A'P)0'Q)''Q)'Q)'Qg)'Qk);'Q)'#'Q)'Q)'f'Q)A'Q)'Q)'z'Q)0'S)'Sg);'S)'#'S)A'S)0'W)'W)'Wg)'#'W)A'W)'W)0'X)A'X)0'R)''R)'R)'R)A'R)'Rg);'Re)'#'R)'f'R)A'R)V'R)'R)0'U)'U)'U)'Ug)'#'U)A'U)''V)'V)'V)'z'V)'Vg)''Z)('Z)'Z);'Z)'Z)'f'Z)'Z)'z'Z)'\" 0'T)'T)'Tg)'#'T)A'T)'#'Y)A'Y)R?+?+D9+42+O9+ + "+$? +"+-? +9+2S/Kֱ02K +@ +@7 +K +@K +@KA +)+ ) +) +T+K<99) 59$9D?79$H99-9K9O9 90146323263232632#"&43232>54&+"#"&#"#"54>7654654'&#"./,7P C]!1*C  '?6"X51   >=)< *#C0eS  -H}"(;_+3>P+2+ +33E9+52-( +W3-d+O2`/"ְ02<I2<" +@B99- 901430232632#"&#"#"54>76=4.+"&54;254654'"&".327654&+"32632#"#".#sy$$W0q= $  * /  Gw.7{a$ D  C og;P*#  qSH-5KexK (A1i+J3d"+@O22m+G3r9++9+ 22&+ 33Ym +d+m +6|33O+122/yְ2^22y +@ +@g +y +@y +@yp +U+22=.:22=U +@=) +@=C +U= +@U +@UM ++yj99U Ki$9=#J99dmCp99r?Q99Yau99w:999+99& #)$901463232632;254'.543232632";2+"2"&+"54>7>=4&#"#"32+"#"&54326546454&+"54;254&#".326;276=4&+"/,7  ""?C --3 Q F  g>%,Z7   ;7 *  &6F     TG:%)X M " *I9I ! }N+H3S9+D2 +3p2p +@p| +@p +&2H +`3&e2~/ֱi+Z2#52#i +@#- +@#F +i# +@ic +@iP ++iNU]$9# K99SNF9&2-b99p99 9014763232>2#".'"&#"3:6;2#"#"#"&#"#"546762>76=4&"&#"&46;2654&454&+"#"ZFD^    )# 3 :   T K   ? 2# &. _6!;  V         )F  &2   n^+3Q9++`33"+d{22^Q +@^[ ++g+&2+3 "+  +@ +n[ +n2H[ +2P+H2 +@HB +2H +@29 +/ִx +x +x} +xh+22M)J22hM +@hc +M5+;+@2;#+++x9h `$9M^95&2DQ$9;=>B[$9#9^c}999Qfz99nMVXx$9@9HDJ$92=>99 ;$9 90174>7>?654&543263232632#".'&#"32674>32#"&'&+"32>32#"&#""&54>=4&#*"#"#"&#"#"732632654654&"%!|9>*c k/>&  $#6P 8&n2@ ,=  48  Z  p #8  + )  6/3 7#H  C    &r 'Wh R+E9+U+[RE +@RO + + 3+fP+ +@ +&22A)+/+42/++j+^XUf999AR99)& 8E$9/126O$9 932#"&'&+"32>32#"&#"#"&732654654'.#"'k [&9c k/ >%  %#6P 8&n2.ZeUrH&,?(KXt + ) 6/3 7#H R"c^U 2+53%9+2% +@2/ +++U9+V/<ְN2"2"< +@" +@" +<" +@< +@<7 +W+67+ JC JJ+CC+BC+JKJ+KJ #99BC99BCJK........BCJK........@"<299%27:99U *,32#"&#"#"54>54&54##"&54?>54654. 9' &i |7M 57n&50 : #  (X+ 2g=  0   N ! 40^mW+3J"+2]+"+W+0+((0 +(+ ++>d] +>g+n/ִH +H +HU +H3+%N+o+H 93@_ak$9%>df$9](%3997P999J9>99d  999 k9990174>767>767632#"&#"32632#"&54>54&'&#*"#"2#"&#"#"73263254/&#"  a  Q?8 / A6;-* %1?  ,67  Z#> '02   =.+ ,;.Y4(   $"F4{Wj+m3Dj+q"+]+UU] +UX ++ 3+z9+ +@ +%;m +%P+;% +@;5 +%; +@%, +|/tֱ@=22t@ +@t +@to +@`+RR` +RZ +R(+.+32(d+K +.++}+@tj9`%D$9Rgh99d79.(015P$9 9KI9jUP`99Dqr9;3IKt$9%0199.w999z9 99014323232632#".'&#"32674>32#"&'&+"32>3232632#"&54>54.'&#"#"54>54654&'" *c k/ >%  $#6P  ("!2%,1#" <4@,  + ) 6/3 7#!&$"&2"5  c"4O!+=3"+1+)g+)1 +), ++ 3L9+2P/EֱE +@ +@ +E +@E +@E@ +E4 &Q+E4C9&8:99919!)49B99L90146323263232#"&#"32632#"&54>54#"#"&547>54654'&#"./,7  #" /&. 1    6  4)  5"!4 ,EBl"4AdN+Fg+FN +FI ++,2333c9+ 722N2+e/\ֱ\ +@ +\ +@\ +Q+CC+:+: +@:4 +: +@* +f+\9QX99CUW999,@KN$9:/9F@QX999c:\999 %499901432326323267654654.'.54323263232632#"&54>54'&'.546=4&'&?1 A1I$ kC&56>WL ,/  .z&  B+  6#* QY <u"07bVgx +j3R"+ R +  +4/a3%"+2%4 +@% +y/ִW+WM+ P22H+h2MH +@M +H9 \+\/_3339+#2Hp+?+z+W99M R999\Z999HK999v9p &.23254654'&5467654&54323#".'&#"#"54654#"&'&254654+"32>54.#""  (W?-      9"D/  &<;A_ '    'c"K*<       3@  =.F & *  6 H   &( a79 K+ 9+//ִ++++ 9999014632#".732654&#"Y@?EME'8-?0).:2,.Njw\`!7>323#"&#"#"54326765<6454#"#".R.    # ( 3  dd#N    ,t)4 $ */>5k+15+1 +@) + /O+  +  +6/ִN+ + +7+"$.99919 +9990154>7654&#"#"54>32;:6267>32#"&#"#"*#C!&*'7654&#"#"54632#"&( !47(/   %5I%5  /_@#? N'%0  ' "8$## 2HaWB8;J/23L+>2 +@( + +@ ++K/*ֱ#2#A$+A/03 2+ A +@  +A +@A +L+A*(2H$9#'9999<99901467>3232632#"&#""&54>54#*"#"#"7;2=7>54#"1     %1 AQp A dn   35   <%:A/x"H?&++7++6 = +65+0d+@/ִ.+. +. +.+:@+A+.9 6=$9 :99909&6.99990143232654&#"#"54>763>7>32#"&#"372>32#"&/$0=+*  ]   F   7!`=9 J3&2/R%   9 &$,U_~6A-c#+9+)/9+./ִ@+&+$+/+&999 999)#$99014>3232>32#"'&732654&#"-AJ8 6).*0CP:+H>0$"$2*/";e?- #TJ;@Y6R547)1M8W-,&+7+& +@&+ ++-/ִ)+)+ + +++.+)9%&99#9 9&99901467632;2?2#"&54>54&++"#"8  1. _ ! =>  7  +3)  0$-8)6/!"+'/.f+4/ "+7/ִ+ +*$+$+$+1 +8+*91 !'$9$9'!99.$94 99014>4.54632#"&732654&#"732654&#"$ !D0:2//?H8J+=% +\)'! !!"')*4=;&$ 8(1R?J1<-+*:0% &#1 |:8)o /"+/!P+'/9+*/ִ$+$+++ 999$$99!9'$9014632#"54767>54#"#"&732654&#"L>?R=:$O ==#%:A73%'%2)$%=X_C?{)  =G QT0Q4(-O2"m7=7v=R|=*/{=()u =Bu!=/""=6~#=8j$=$-u%=:u&=Q},4=,5=}1/  +/ִ $+ + + ++014>7>32#"54654&'&  = %  //M , ./++/ִ + ++0147>32#"&'       ' ]!/ִ + +  ++014>32#".'&( %-%%,% '$45dC4 '6tGAq;*8E_ {]$/ִ+ + + 2+014>54.5432#"%-%%+% '$&( '6tGAq;+ 7F_/5dC4'X]/9+/9+ /ִ + 2+ +!+9 9999 90146;26232+"32632+"&' 7 . <8   UX=/9+2 /9+/ִ+ +/3 +01430;254#"#"&546;2+"7 / <,#   'J6=J{7='P8=PU9=7ORO*/O() OB!O/""O6#O8|$O$-%O:&O7~R ~* /~() ~B!~/""~6#~8$~$-%~:&~c}>4O>5O}m4~m5~,(z,(V,(V,(V3hqz3hrz3hsz3hszIe<? ; e?!/ִ+ + ++014>54.54632#" )::)(9:( 1?:'*J  22+?   C7 +2E/+23/ ִ%$+22 % + +2 " 4+90147>5454.54#"&543232632#"&#"#" " 'I( ( %# &!q ?+22  J>7C & ./++ /ִ + + +0174632#"&&"&  *( '*'d)Amz'T}zz'z'#5DU +H H +@ ) +&+/++BP+B +@ + +V/ֱ6 6?+N+ W+6:(+ ,', ,+ ,+ ,+9+ ,+ ,+''+'+&'+9+ ,.,+/,+9+ <,+=,+'E'+T'+U'+., #9<9=9 9 9 9 9E'9U9T999@ ',.<=ETU ................@ &',./<=ETU ...................@?6 )2H$9R999N9H 19B2:R$99017463232>?632#"&/"#"&56?64.'&7327654&#"32>54.#"'l &&h'  1U!} 3 D_n6+? ##yr   *G)#4 - 3W'R,#  U ",?'+P'*Z74>3!2#!". U r  t 4632#"t# A%# %TA]747672#"'&'& _] *40y hi %.)<+i4632"4632"&""0 "  4>32#". &!   *H' &% (47>32#"&54>54.  '! 7%*H' &%747>32#"&54>54.  '! 7%*H( &%:v 2w A M t .  d .   % O 4LCopyright (c) 2009 Barry Schwartz (http://crudfactory.com)Copyright (c) 2009 Barry Schwartz (http://crudfactory.com)OFL Sorts Mill Goudy TTOFL Sorts Mill Goudy TTRegularRegularFontForge 2.0 : OFL Sorts Mill Goudy TT : 9-1-2010FontForge 2.0 : OFL Sorts Mill Goudy TT : 9-1-2010OFL Sorts Mill Goudy TTOFL Sorts Mill Goudy TTVersion 003.000 Version 003.000 OFLGoudyStMTTOFLGoudyStMTTCopyright (c) 2009, Barry Schwartz (http://crudfactory.com), with Reserved Font Name OFL Sorts Mill Goudy. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFL Copyright (c) 2009, Barry Schwartz (http://crudfactory.com), with Reserved Font Name OFL Sorts Mill Goudy. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: http://scripts.sil.org/OFL http://scripts.sil.org/OFLhttp://scripts.sil.org/OFL1r  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghjikmlnoqprsutvwxzy{}|~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~uni00A0uni00ADuni00B2uni00B3uni00B5uni00B9AmacronamacronAbreveabreveAogonekaogonek Ccircumflex ccircumflex Cdotaccent cdotaccentDcarondcaronDcroatEmacronemacronEbreveebreve Edotaccent edotaccentEogonekeogonekEcaronecaron Gcircumflex gcircumflex Gdotaccent gdotaccent Gcommaaccent gcommaaccent Hcircumflex hcircumflexHbarhbarItildeitildeImacronimacronIbreveibreveIogonekiogonekIJij Jcircumflex jcircumflex Kcommaaccent kcommaaccentLacutelacute Lcommaaccent lcommaaccentLcaronlcaronLdotldotNacutenacute Ncommaaccent ncommaaccentNcaronncaron napostropheOmacronomacronObreveobreve Ohungarumlaut ohungarumlautRacuteracute Rcommaaccent rcommaaccentRcaronrcaronSacutesacute Scircumflex scircumflexuni0162uni0163TcarontcaronTbartbarUtildeutildeUmacronumacronUbreveubreveUringuring Uhungarumlaut uhungarumlautUogonekuogonek Wcircumflex wcircumflex Ycircumflex ycircumflexZacutezacute Zdotaccent zdotaccentlongsuni0218uni0219uni021Auni021Buni0237uni02BB afii57929uni02C0uni02C8 gravecomb acutecombuni0302 tildecombuni0304uni0306uni0307uni0308uni030Auni030Buni030Cuni0312uni0315uni0326uni0327uni2000uni2001uni2002uni2003uni2004uni2005uni2006uni2007uni2008uni2009uni200Auni200B afii61664afii301afii299afii300uni2010uni2011 figuredash afii00208 quotereverseduni201Funi202Funi203Euni205Funi2060EurouniFEFFf_ff_if_lf_f_if_f_lc_tf_jf_f_jf_bf_f_bf_hf_f_hf_kf_f_k bullet.001a.scb.scc.scd.sce.scf.scg.sch.sci.scj.sck.scl.scm.scn.sco.scp.scq.scr.scs.sct.scu.scv.scw.scx.scy.scz.sc ampersand.sc dollar.lining zero.lining one.lining two.lining three.lining four.lining five.lining six.lining seven.lining eight.lining nine.liningi.TRKkcommaaccent.sclcommaaccent.scncommaaccent.scrcommaaccent.scgcommaaccent.sc uni0219.sc uni021B.sc ccedilla.sc scedilla.sc uni0163.sc agrave.sc egrave.sc igrave.sc ograve.sc ugrave.sc aacute.sc cacute.sc eacute.sc iacute.sc lacute.sc nacute.sc oacute.sc racute.sc sacute.sc uacute.sc yacute.sc zacute.sc atilde.sc itilde.sc ntilde.sc otilde.sc utilde.sc adieresis.sc edieresis.sc idieresis.sc odieresis.sc udieresis.sc ydieresis.scaring.scuring.scacircumflex.scccircumflex.scecircumflex.scgcircumflex.schcircumflex.scicircumflex.scjcircumflex.scocircumflex.scscircumflex.scucircumflex.scwcircumflex.scycircumflex.sc abreve.sc ebreve.sc gbreve.sc ibreve.sc obreve.sc ubreve.sc cdotaccent.sc edotaccent.sc gdotaccent.sc zdotaccent.sci.TRK.sc ccaron.sc dcaron.sc ecaron.sc ncaron.sc rcaron.sc scaron.sc tcaron.sc zcaron.sc lcaron.sc amacron.sc emacron.sc imacron.sc omacron.sc umacron.scohungarumlaut.scuhungarumlaut.scthorn.sceth.sc dcroat.schbar.sctbar.scae.scoe.sc lslash.sc aogonek.sc eogonek.sc iogonek.sc uogonek.scdollar.lining.supzero.lining.supone.lining.suptwo.lining.supthree.lining.supfour.lining.supfive.lining.supsix.lining.supseven.lining.supeight.lining.supnine.lining.supdollar.lining.subzero.lining.subone.lining.subtwo.lining.subthree.lining.subfour.lining.subfive.lining.subsix.lining.subseven.lining.subeight.lining.subnine.lining.sub comma.sub period.sub comma.sup period.sup parenleft.supparenright.supbracketleft.supbracketright.sup parenleft.subparenright.subbracketleft.subbracketright.subzero.lining.numerone.lining.numertwo.lining.numerthree.lining.numerfour.lining.numerfive.lining.numersix.lining.numerseven.lining.numereight.lining.numernine.lining.numerzero.lining.denomone.lining.denomtwo.lining.denomthree.lining.denomfour.lining.denomfive.lining.denomsix.lining.denomseven.lining.denomeight.lining.denomnine.lining.denom comma.numer period.numer comma.denom period.denomhyphen.uppercaseuni00AD.uppercaseuni2010.uppercaseuni2011.uppercasefiguredash.uppercaseendash.uppercaseemdash.uppercaseafii00208.uppercaseparenleft.uppercaseparenright.uppercasebracketleft.uppercasebracketright.uppercasebraceleft.uppercasebraceright.uppercaseperiodcentered.scldot.scguillemotleft.uppercaseguillemotright.uppercaseguilsinglleft.uppercaseguilsinglright.uppercase oslash.sc equal.ref1j.ref1guillemotleft.ref1 divide.ref1quotedblleft.ref1quotedblright.ref1quotedblbase.ref1$,q dDFLTlatn4 AZE 8CRT 8TRK 8  aaltbaalthc2scncasetdlig|dnomfrachistligalnumloclnumrsaltsmcpsubssups      (08@HPX`hpxJ*V`jLxZ<@ >FJRZdhrv "&*.26:>BFJNRV^fjnrvz~  $(,048<@DHLPTX\`dhlptx|  $(,048<@DHLPTX\`dhlptx|  $(,048<@DHLPTX\`dhlptx| '^:6_;724TRV35US(H>)I?*J@+ KA,!LB-"MC.#ND/$OE0%PF1&QG`<8a=9>bcfWdgjj      ee    XYZ[\]hiHIJKLMNOPQTU  $>@@-D^.``ImmJooKyyL}}MNel  =@Cot  >GRS">FJRZdhrv "&*.26:>BFJNRV^fjnrvz~ "&*.26:>BFJNRVZ^bfjnrvz~ "&*.26:>BFJNRVZ^bfjnrvz~ "&*.26:>BFJNRVZ^bfjnrvz~ '^:6_;724TRV35US(H>)I?*J@+ KA,!LB-"MC.#ND/$OE0%PF1&QG`<8a=9>bcfWdgjj      ee    XYZ[\]hiHIJKLMNOPQTU  $>@@-D^.``ImmJooKyyL}}MNel  =@Cot  >GRStL RS>?@ABCDEFG TUHIJKLMNOPQ RS TU">GRS HQTU  HIJKLMNOPQTU>GRS (6745 !"#$%&89 >@(':;23()*+,-./01<= >@ Zqq xdj   e  xDEFGHIJKLMNOPQRSTUVWXYZ[\]y   "$&(*,.02469;=ACxdj   e  x $%&'()*+,-./0123456789:;<=y  !#%')+-/13578:<@BV(^_V`abcfWgXY[\]hi >@^`mo}oprst~ $,4<DLRX^djpINIKIEIMIOILKMNEOLIIWF F`DFLTlatnAZE CRT TRK cpspkernv  | "$%&'()*+,-./0123456789:;<=c  !#%')+-/13578:<@BH2`v.4>D HRhz $28>\flr$6LV`nx^9? [^ 9??[: ) =4S[wx?%" [_ ^9r?Ep^9?Mw?H_9?pE [E_  ;%9?  ? 9? 5;=") % 99 )EfM s+Xy{`gI  =:w ?5 9?.46"9?9? % EQM^CdfKR4=9?yd+e 9? 9? "9[9? 9 ?E|~bGhjOV8 =    Ei|N 9_9pE!#_ ?9gpw .4 SLiFwi?9?pp  #+)  *20   9p5w?0 ? Ep| y+,_dRe) 9 9H 349?PTY[ip| %9=>? ^_egiĸtkkDkkt:aM'D:MMDMaD'$ &A?D$ &$ &$"'$ &       #!&   $ &$ &$ &ZIIII]5II,,IIII,,5IIIIIIIII"III?I"III$ &$ &$ &$ &$ &GdPddUZdddZdPdiiPPZZ$"'$ &A?D$ &$ &rm__ sn`__  #!&$ & $ &   $ &$ &XXXXlDXX;;XXXX;;DXXXXXXXXX 1XXXX1XXX$ &@@@T@@#@@@@@@@1@@@@@@@@@@,##,@@@,@@@@@1@@@@#$"'$ &$ &$ &$ &rZ$ &$ &$ &$ &UUUUiUUU8UUUUUUUFUUUUUUUUUU.A88$AUUUAUUUUUF..UUUU8A?D $ &sZ rrUrr$ &$ & Urr Aum?DmA?DA?D$ & J A?Dkoz$ & 2 LimxKIr }imximxFJiiFJiPiZsvnwUYiw78UAUUKUUUKUAUZZAAKKgaJ}1S0C{//)YkC6IOQHPR!I#d%&b4qpK[@*]Fz3Uj?gqe IG$T.QH!IIIIIIbh@:@<=@`X^iV@;>QQQQHZHt**** \A"_F9W,!!!III32ddc&jbgb4 4'45d(J8a8TL + -FFnwyD)|MvmNxu~~Els]MmyNxNxuMmNulsMuuluyD)Nu~lyuysMmNxsuuBxuTTTTTTTTr7qqKoKfg m 7AmD):z,z=@9J@y=>::: S S S  S T !$l&'+- *(UF:zzz38W ::z?zY:Ka : (z z"H SQ:z#Q: X P: S SSSSS:Sllll- 2.::10::z}zzR4c:zzzzz:zaaaa<;:SzSzSzSz z z z ~ z z |SSSS      : 8C     : : :nSzSzSzSz Y Y V!:!;!:!5$K$K%Mlalalalalala'-- : : 62/!:$KZzzzzzzImEGImEG [,:z`:33333z33333333 ^z^wgzdoN]j_hzd{vbfLYB 8]jhvzbfzbfwtzzwojhzvbYuhzwrzzwzdpOzbwzizzwzYoz^whvbfYjwqzzs^^dfzjwk  [[[[[[[[\ ( (xeUUzBDa?ci]kpdrujwyn{}qDto8JLM=OVjMgnslixmpp-1.2.2/docs/_static/ir_black.css0000644000175000001440000000776112473141653021053 0ustar mathieuiusers00000000000000.highlight .hll { background-color: #ffffcc } .highlight { background: #000000; color: #f6f3e8; } .highlight .c { color: #7C7C7C; } /* Comment */ .highlight .err { color: #f6f3e8; } /* Error */ .highlight .g { color: #f6f3e8; } /* Generic */ .highlight .k { color: #00ADEE; } /* Keyword */ .highlight .l { color: #f6f3e8; } /* Literal */ .highlight .n { color: #f6f3e8; } /* Name */ .highlight .o { color: #f6f3e8; } /* Operator */ .highlight .x { color: #f6f3e8; } /* Other */ .highlight .p { color: #f6f3e8; } /* Punctuation */ .highlight .cm { color: #7C7C7C; } /* Comment.Multiline */ .highlight .cp { color: #96CBFE; } /* Comment.Preproc */ .highlight .c1 { color: #7C7C7C; } /* Comment.Single */ .highlight .cs { color: #7C7C7C; } /* Comment.Special */ .highlight .gd { color: #f6f3e8; } /* Generic.Deleted */ .highlight .ge { color: #f6f3e8; } /* Generic.Emph */ .highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */ .highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */ .highlight .gi { color: #f6f3e8; } /* Generic.Inserted */ .highlight .go { color: #070707; } /* Generic.Output */ .highlight .gp { color: #f6f3e8; } /* Generic.Prompt */ .highlight .gs { color: #f6f3e8; } /* Generic.Strong */ .highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */ .highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */ .highlight .kc { color: #6699CC; } /* Keyword.Constant */ .highlight .kd { color: #6699CC; } /* Keyword.Declaration */ .highlight .kn { color: #6699CC; } /* Keyword.Namespace */ .highlight .kp { color: #6699CC; } /* Keyword.Pseudo */ .highlight .kr { color: #6699CC; } /* Keyword.Reserved */ .highlight .kt { color: #FFFFB6; } /* Keyword.Type */ .highlight .ld { color: #f6f3e8; } /* Literal.Date */ .highlight .m { color: #FF73FD; } /* Literal.Number */ .highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */ .highlight .na { color: #f6f3e8; } /* Name.Attribute */ .highlight .nb { color: #f6f3e8; } /* Name.Builtin */ .highlight .nc { color: #f6f3e8; } /* Name.Class */ .highlight .no { color: #99CC99; } /* Name.Constant */ .highlight .nd { color: #f6f3e8; } /* Name.Decorator */ .highlight .ni { color: #E18964; } /* Name.Entity */ .highlight .ne { color: #f6f3e8; } /* Name.Exception */ .highlight .nf { color: #F64DBA; } /* Name.Function */ .highlight .nl { color: #f6f3e8; } /* Name.Label */ .highlight .nn { color: #f6f3e8; } /* Name.Namespace */ .highlight .nx { color: #f6f3e8; } /* Name.Other */ .highlight .py { color: #f6f3e8; } /* Name.Property */ .highlight .nt { color: #00ADEE; } /* Name.Tag */ .highlight .nv { color: #C6C5FE; } /* Name.Variable */ .highlight .ow { color: #ffffff; } /* Operator.Word */ .highlight .w { color: #f6f3e8; } /* Text.Whitespace */ .highlight .mf { color: #FF73FD; } /* Literal.Number.Float */ .highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */ .highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */ .highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */ .highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */ .highlight .sc { color: #A8FF60; } /* Literal.String.Char */ .highlight .sd { color: #A8FF60; } /* Literal.String.Doc */ .highlight .s2 { color: #A8FF60; } /* Literal.String.Double */ .highlight .se { color: #A8FF60; } /* Literal.String.Escape */ .highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */ .highlight .si { color: #A8FF60; } /* Literal.String.Interpol */ .highlight .sx { color: #A8FF60; } /* Literal.String.Other */ .highlight .sr { color: #A8FF60; } /* Literal.String.Regex */ .highlight .s1 { color: #A8FF60; } /* Literal.String.Single */ .highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */ .highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */ .highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */ .highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */ .highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */ .highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */ slixmpp-1.2.2/docs/_static/header.png0000644000175000001440000004031412473141653020520 0ustar mathieuiusers00000000000000PNG  IHDRgIDtEXtSoftwareAdobe ImageReadyqe<PLTE@:F?IDATx]ks9>_SSB+d0² Y6ɛIӗþ䂍"ktO4M;SHq%?M8wBc)撷BnrYrE+ZޑJ8¯83 Ap>^Nc# [WcIjէhJ͸¡-N~>!4'Dmp ุQT+<[3M%NdMR qq~p1Ĕ -+I-P()ʎjMHD kK)lW$6Z+׋ZN+*!a5i,I,CN% B$IN2ɱ6G$iyM{N׼i9q_⎂S—iCG3*[ڻjîsY(Lk %Fȡ{gI"+2Iaq#H8Ygen֌I؍#^+?_ "鷦!]@ j\h%w!jK_!*$@s}1ζ+g]__;m>;u}a6ST*qIkJ]ȕr&k{Zy|,3"6o|R򊭃6ڟ$imi(dkڈ:1k|ޭ+*H[*`jI$Jl_X;Y+czKԜu+`Ls j?<!ݟ(w)3$*8md_{ f k'!=dmVuu 3ƈߎZĎ5.έ5dMk2_  (\Rc;|H  |%^V|E v|V&K()Ҙ X)b TTKh^4d!|QR.!El'@هYe`KqK!'m%;p8*;Pѻ_n1?rم-)Fe<,]~E1xZ2U} U사;f:,`hVҦsLu;!1a~OЦx0ăC*+4%c8張Y EOڔsZC /-^ȸ#nju6Հy/Hbr;l7%^mZ،i^XLc`uy>X}g(EjgQ#=/K",ʀM i O%:눑;rDl%y_ 2Er:+t&k##k!cfUg226pc Nm&{;#}!92h<~-zI{,T!>ZDآ ;v2| HϪ ӛĎthG3ldպV:w=@H`o8zY题1[l]@`#TT\Hi->,f` Xٺ Vgm! fe=?]CWvø6p;qY/aaۄ, lTY?ek_kT`r1S;B)CN?gdGӣz $K|CRYw̺5-܅rɽ?f۹ Uȏ]3ec&LX ݾ'D盚.1 :Y HifjΪ#ꊲ d$ c]ܕLYg䂽a6uHEZ,4շUg!qSk]R6C (>>΂n.?g >ċHKK.N9fG)F?ٴác1BFaF1fpڔs).ë7Aop4ЦBp*u; }v1wL}8;zh!ogOݜ‌Y%S^i\b1g\V#Xm^췼m;$ ,`v 7ۀ 8WCZʈlRλg[8 ً撨I,IjHqBemO!^.O9E޷OBБ@%W هx @ɄU 2Zz +&HF*0Wj^=r'}|}i;^fL=.0`,P_nځ˵ ZFU*Ie"%4\<3SH3,hsm)FFZq=jK)nG4A(=1DGCshP*?D gaba~f+2)ŅwNY]{9LƬT1ږC Y;@:_Di짊b3<-2~rJ9$1u hॲ|^N p¦xZ׭>" Ġ$&rٓ{ PfVZ2}`c4c³' 5<QH)Y-B 1Tk9LbUk;I>$V }re-98v|r9Wc04Rj61%K1ʫ1TJ$5R1Z˶5v1AYlXUѪD $d`.\Ruoy w_*fYj̓&M/Ҳ #Kz,Ttr~mSz䜼9oP`Y~Mk [uߓ[aV;h2;w';{^jwNiJ/FӽQ*î!EۏsQ@n^>~vzBهe?G1񐌭FC!NCʹ)6ErSl9MZ1cexxvt|Bh%83dSX`5gο]\oBY_}xE?{&1lB`*U/V{޻MNs,]<֭HijiNj2p4o[BXwEx*l1`,O~ e~rY_6/MS1,1՘ƚwW 2λ΂.^9CK)I]jLه`vz)EN- s 'b-K*Lα^ MXύxVar8_G8n>ETGrЋwAG#Q=8Kr}>;yEVL$c<i lcQ| gAQk.Fgd=nM-`!w[Q`oa*#mn><ɲv7u_mtƎ(Cpٶyʕy12։vOk Ƨ4` d]'* Q uBԝj~Pd* oxZ5T77mptQO9Ʊ72 "ہ8M%}lX&9\, [[ L"/}gk{m3{CaQڼuXђKA=<]_!\/)W3{n}l/5wTy{096m_q1hc.6Dok% LM;xY:Jw'CZ.rpUv~s\`Z-r?L<6td6EgXjq;2+i)ƍ j`d2Sk/-e n=(]^$T2;8 {nu|zCŦ\[1n;E՟^;,X(k]r .%7[sm륛"t|"ˡβBCHDK-!_[~:z0N^x6%gNJzcxGU\Kʰzg|!p8jk3ΦpXZ6dZ.#&^׏$v#^}-eamfmF`B@az?΂NN=#= _/@OpݴA^L!%XwF_Z)]?Z1g/ D+ʹʹʹ>S8Q`fm I,[HXTL%5rV1BVܑ8>ݓ B w1 KTx&?rWQi{>U2=8f1+Ɋ \88.@.(/~{Fa7*ZMQ:~Bc9/sHm,e@-S3zzO1ȔPi)X*+V!S]XK׭SkV9])"&ofyp6O h)DfMqʖ^[B b)#R_:\8?wфmhSI5ժi1ό<ЂN1&78VPC̟Z,Ƭ*};z5@vh帛1vG;zcm=QM9`]87p."2l9!"R4)E; OH_z‹G@ m֛۷}ry{DN-"O7d&ky@#]ӵn!&cz,jDg| 65H IZ&/O_Y:ZF2ˌ>̪15gf*/'A!bm٭!X^[696Y2#I^8M acA dkUI 23Q#oDEFa]MΒkng y%O jhޛzU$D\ʃWw̞ԗm})y3byo*l ;$BV4ŤR) jl~Oıdme3v$bKs #F) "KJvb}ÎRJy<8;ߙVD6m$Lmci# wL>X.?GDZL2FwUbj~" /Gy]vu*6{}xYz2*uE:>H)VAm1vE.οQl>GbAOF3dA[=f'-D?@!o9geQ6G{K+ ggj@LrۢeŠE9¶'`V+̰0l$Ͱ.-Hi1Fˣ[ӗ}|Mh~kejիNn~nC1SWzWD=wTw{<VlEuҲ&P#ȣ}ZٕRz`[0qneUX S2;񩌤w(% ڛֶH$cqvhWih> g!'C@&Lx,wJxP'j;Qd#81S%d.qtoTj5}JyosE1Q#< :36nչs= ngv^bуicaD/!DVxe&;!"5: `uU,1/> v,ِ^\Fj@'RRql>*6z]76{1}nq? rv $}s U=B!QA. ֮!q^cmCWf!5$_EJu4p|Sg)o |H{H,2.#5[ Lr/ψTfMwuI`8Kf=/h~^ Co44 )ª)_v2Onin#&D`F"${smCltW,⻆mcbzn/6T%yK$?˳Ql~ W4Rm:[F'v7.]y%3doݼ^ (C8}IQEkW<)5>[x 3Q{EHJ#Ջ<gϽ mSӴi۟&z8 ~<'RRbK>}=4V  fEfkho{g{6 6jr*؈_<- !Yi [nX1oeֆ -ѹO8q6s7O7q十>_„|97{ \k'cÑX"89ϫh;Yc&gjHr;rrSE][C q!I.Iq'6S׃Ǟn_JB{x9]D·l2<vWpl2v &>ˌ`xGb "mIKkN%eaR}a]ɓq6+l-!4g;ܚUt1_-JbofyS#"#}+źtrA;_emqj.MH̞jY`4+-#Ag_c̈³M Ċ#yǷ0]N(N)`@/fCۃ[ܨj& lpMAcC퓉OgږD/GcǫyӷS6P gd~T?:!\e#xgBh 6}1t6'X0/9.%,<7ϟЎgc4RV1/E!`I r=z\AK"^4g`K7f2ժMAXTW1UEЭqc}}]g&&p²}bA$R{j/#󠢱e],a=}7E}HbU&5b._;f^`{ۑngpK#Bpd=Y4cm*ۢ_[8LQW+L;3 rLBZ}Oޱe S4Co(ܲ6|.c&0[dz_61ΫYvY}׿ >n0P/³k 2P,R4+`04Ћ43$ J-SXXg+F៻R繥MضRFnԏ.q[+yoCSPB̳gL|A1F̌+1]3>B&GC۞ JbDiz3[W)[8dMpbviv腐֕E!Ow]p&_UblSFmTKlڕB&>q>ig,Q;W,km 4%^RQ4@k )<{/fPkzВA± ٘bXZ,N/5& ~)枥;99օĺI}܎!:^R(*L{cU߿HSW%ISx~,dL)yL~ƴe2K'Ĕ|`3ش澉 k8kz[/'^˕G&FjaNW뾞Y`_ b0l*g]y-7~(YDsS߶~7 2(T+{z˟I3 MAfcCSSe Yg[bʷdYsj+CP*zX1Q4~T72L;ZgGW1i{a.}Ԩ}$_kgcVd^O³_"d&gAp3-&⪒{CncOk\uLdϋn\qx'DKtW5 [\!ʕrmm\P^W P ǿ[;a@S3oZZNWJY*`ӵ\$%*g.NމO2mYm8R8&ʗ9ED[;fғMlX :%Z/]UzN;FqSx0)dh̄HH@hA7~mT-1)(0,) 9Os6"N1U,  D$B0[`$a_K[/1jrZI@גO4GO6[j[ce;gE*k+i[}85, ~ۨ 0iEDH ^  :M$m q pS"C|u]l$#$ڎuW3ն)k(Ǫ=Jkp>7)h{k}gYJ#) i^َr$* /X(Sbq؞5k+oX[W9̇t-yX~"@ K˹4s(ջt=YاFx5k ~sQ&G&Wt6M/&N@ V=좞ev#A][ErP2قW|*HCɲ,Khj:/Ϧ_&g㣽7%5YCʵʵkN_F2kwAK^K?DRM( X.+i.ېK{Xco/rwR[7pf?]N{#f0 c–UIf 6Hl}jÉyPqtF=NƇoeB% :#1so |x6A-]'=WOK="TK(3"dĪ2OƏQmj³= /w>]ߡ[0*GɴwvgMA/) Qr@\>eF&>smx^(5{\#n@N\6h Zu%jC$bW}O7lz=<$km2Ya6<|^g uR-7³߃3~sX>]LO5i%[Q|DET:눮5֥*3gSr$zՊ?m8tdtr:@fߗMƲ!$tz#DajVډBvl}5fD6?"Gi! ׋ſ>,7X,旧v{R[̞5I[x"³؏d嗣ĢV'Myw")(h\o1'6cgG2 V-"M62Cgd9WS~N "fKuX0hn'® ^Ձ cEjkQ1K{³`q6B@"K"k`7A|9"*㡱CXx,g*K\ }WjC$ldos&2"r-%V<(Eׁ`L0~֮8SEQiYnNc lv7?AY$A dCv$Z˵xlWm:5/w&l!rf*r&Y$ư>8O?&^r3E$F⚠Q ղ'>tz>?B>mȾzae*rY"eQr vzTg^T2 MҒT]2^st;ׄP=:UWxD!s>PލKsy~w"[³]tKD;LXy:{ ,X+Ah%Ve\p⥴$]D 9kXQEaעʘjy8/|N+s\$gٻfbMVI>olU|ʍԇyHkбHR)eUe4105'ddr^31'%Lgep:wy C9}an΢"$ܚy&@Dzu:vEY.O]CGfv!t#"R%fRFnq}׬-Q+Ekǐ6_YC>c9\saAV曺%X ֲ% b2[αw"2Py*S-+i.<{l>bU: cO9/?CR$qHLFuxO쭋A^W?u!>jNֱO(hjC)P[=VQa APh*LB{EG %B|Y{۫q$+|x{۶jgHu5",Nd65Ue=|w>)IVQn{& b ǎW„@|;[kb#}." F{lmMdЅ٤LC2xآ9))`ǽ㓓pc*B2"UNV;8( Eƽ@YEez vK$ەA#cFY,EM@]ݘ|Q KE|Kq&(w^^ޜr5;}9%0vӝ%zM| j0T Ɗ= "MU629nWYK75wˑK8Jw;% лc78~/GTgW=Ziʺ57wQد&v<-[ HCۭFDc5]QgX@s( cAi/J#U45}/EnzZiĆf y!cUgWKkF ߌEcEy&+-;OՋ%\s0F׺ţ(5AbYM1U0h%Oʙ.6l(7ex*~˽8]ӿaf`raz3:J4[=\`1lnk،^{SLݥO* w&:(ʵ0&,9G.zc{Uv).㖾wlb(^QTa(+o_$"tfQ]5U@Dw4{"'S(362UϏVY .H}uۜIrĜnEtEP%!SI;'qD!ްi[Ud1P~Ghd{i;Eޢ1[)%Ɗ,(<=V4$B Y T&N_9P[>Pb q,pp{>&B0>x~?ή(}9 ?ETSq#S)ɖUE:ON >0bXZއ`YN|"&)[#tݔqr i[ٕ?=%2g#qKcDç(?sѮ.>" cX,YoiP &|?lWQh0bVchmILx;!4:L6NQ?:X Hl\K!:1(ʷ+5Ypz1=9^k6w.G<3M' zK(m^ >Bqib?v;eUg͑5MP9xK[WЁvfEy\XCd F +"KLvl<5~?8+ ֜W jTڢtRvi['ǝ$zCrbJ&"y(xVĜ"g͐OD.lv|ILG^̄DmUgl-E<9DfҚ(&CT{F̔<<Դ7 -γwAZf#BxiUgcodo;%n1=V(EQ7+`y׷zˀ2mQZ_:Lt޶OTE3yQ۟(fZZe&`v4"`f cxޟ4d~D"XN3Y#RqUk,ȯb5܌oYuͬmUg*l9J83!( d9ӍXr߿ 3 7fdmp>vWD/P}(2$Ce6P,#x7mZ'wP=+1lXZEQnջmX O6,\?ۣY۪>q6/+2QUѲvvmo#p޴#Xv>z1!xf2(ewovհiUg) m.8j2x*<^n{fbTg@l >%|EQb6oUgˆr :jEQEuvI@bI+v ծS(kV0R-:!02 .?>Dyos@g9i?"[@gRwQ#|]@gsD1T3Plc(DNC) m>i;:_Yoޓ(tj^@g{r5k ruk : : : ,,,: : ,,dbAIENDB`slixmpp-1.2.2/docs/features.rst0000644000175000001440000000006612473141653017504 0ustar mathieuiusers00000000000000How to Use Stream Features ========================== slixmpp-1.2.2/docs/differences.rst0000644000175000001440000000320312474402424020134 0ustar mathieuiusers00000000000000.. _differences: Differences from SleekXMPP ========================== **Python 3.4+ only** slixmpp will only work on python 3.4 and above. **Stanza copies** The same stanza object is given through all the handlers; a handler that edits the stanza object should make its own copy. **Replies** Because stanzas are not copied anymore, :meth:`Stanza.reply() <.StanzaBase.reply>` calls (for :class:`IQs <.Iq>`, :class:`Messages <.Message>`, etc) now return a new object instead of editing the stanza object in-place. **Block and threaded arguments** All the functions that had a ``threaded=`` or ``block=`` argument do not have it anymore. Also, :meth:`.Iq.send` **does not block anymore**. **Coroutine facilities** **See** :ref:`using_asyncio` If an event handler is a coroutine, it will be called asynchronously in the event loop instead of inside the event caller. A CoroutineCallback class has been added to create coroutine stream handlers, which will be also handled in the event loop. The :class:`~.slixmpp.stanza.Iq` object’s :meth:`~.slixmpp.stanza.Iq.send` method now **always** return a :class:`~.asyncio.Future` which result will be set to the IQ reply when it is received, or to ``None`` if the IQ is not of type ``get`` or ``set``. Many plugins (WIP) calls which retrieve information also return the same future. **Architectural differences** slixmpp does not have an event queue anymore, and instead processes handlers directly after receiving the XML stanza. .. note:: If you find something that doesn’t work but should, please report it. slixmpp-1.2.2/docs/Makefile0000644000175000001440000001076212473141653016600 0ustar mathieuiusers00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Slixmpp.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Slixmpp.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Slixmpp" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Slixmpp" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." slixmpp-1.2.2/docs/index.rst0000644000175000001440000001430212572154127016773 0ustar mathieuiusers00000000000000Slixmpp ######### .. sidebar:: Get the Code The latest source code for Slixmpp may be found on the `Git repo `_. :: git clone git://git.poez.io/slixmpp An XMPP chat room is available for discussing and getting help with slixmpp. **Chat** `slixmpp@muc.poez.io `_ **Reporting bugs** You can report bugs at http://dev.louiz.org/projects/slixmpp/issues. .. note:: slixmpp is a friendly fork of `SleekXMPP `_ which goal is to use asyncio instead of threads to handle networking. See :ref:`differences`. Slixmpp is an :ref:`MIT licensed ` XMPP library for Python 3.4+, Slixmpp's design goals and philosphy are: **Low number of dependencies** Installing and using Slixmpp should be as simple as possible, without having to deal with long dependency chains. As part of reducing the number of dependencies, some third party modules are included with Slixmpp in the ``thirdparty`` directory. Imports from this module first try to import an existing installed version before loading the packaged version, when possible. **Every XEP as a plugin** Following Python's "batteries included" approach, the goal is to provide support for all currently active XEPs (final and draft). Since adding XEP support is done through easy to create plugins, the hope is to also provide a solid base for implementing and creating experimental XEPs. **Rewarding to work with** As much as possible, Slixmpp should allow things to "just work" using sensible defaults and appropriate abstractions. XML can be ugly to work with, but it doesn't have to be that way. Here's your first Slixmpp Bot: -------------------------------- .. code-block:: python import asyncio import logging from slixmpp import ClientXMPP class EchoBot(ClientXMPP): def __init__(self, jid, password): ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) # If you wanted more functionality, here's how to register plugins: # self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0199') # XMPP Ping # Here's how to access plugins once you've registered them: # self['xep_0030'].add_feature('echo_demo') def session_start(self, event): self.send_presence() self.get_roster() # Most get_*/set_* methods from plugins use Iq stanzas, which # are sent asynchronously. You can almost always provide a # callback that will be executed when the reply is received. def message(self, msg): if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') xmpp = EchoBot('somejid@example.com', 'use_getpass') xmpp.connect() xmpp.process() To read if you come from SleekXMPP ---------------------------------- .. toctree:: :maxdepth: 1 differences using_asyncio Getting Started (with Examples) ------------------------------- .. toctree:: :maxdepth: 1 getting_started/echobot getting_started/sendlogout getting_started/component getting_started/presence getting_started/muc getting_started/proxy getting_started/scheduler getting_started/iq Tutorials, FAQs, and How To Guides ---------------------------------- .. toctree:: :maxdepth: 1 xeps xmpp_tdg howto/stanzas create_plugin features sasl handlersmatchers Plugin Guides ~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 guide_xep_0030 Slixmpp Architecture and Design --------------------------------- .. toctree:: :maxdepth: 3 architecture plugin_arch API Reference ------------- .. toctree:: :maxdepth: 2 event_index api/clientxmpp api/componentxmpp api/basexmpp api/exceptions api/xmlstream/jid api/xmlstream/stanzabase api/xmlstream/handler api/xmlstream/matcher api/xmlstream/xmlstream api/xmlstream/tostring Core Stanzas ~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 api/stanza/rootstanza api/stanza/message api/stanza/presence api/stanza/iq Plugins ~~~~~~~ .. toctree:: :maxdepth: 2 Additional Info --------------- .. toctree:: :hidden: glossary license * :ref:`license` * :ref:`glossary` * :ref:`genindex` * :ref:`modindex` * :ref:`search` SleekXMPP Credits ----------------- .. note:: Those people made SleekXMPP, so you should not bother them if you have an issue with slixmpp. But it’s still fair to credit them for their work. **Main Author:** `Nathan Fritz `_ `fritzy@netflint.net `_, `@fritzy `_ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP `_, and a former member of the XMPP Council. **Co-Author:** `Lance Stout `_ `lancestout@gmail.com `_, `@lancestout `_ Both Fritzy and Lance work for `&yet `_, which specializes in realtime web and XMPP applications. - `contact@andyet.net `_ - `XMPP Consulting `_ **Contributors:** - Brian Beggs (`macdiesel `_) - Dann Martens (`dannmartens `_) - Florent Le Coz (`louiz `_) - Kevin Smith (`Kev `_, http://kismith.co.uk) - Remko Tronçon (`remko `_, http://el-tramo.be) - Te-jé Rogers (`te-je `_) - Thom Nichols (`tomstrummer `_) slixmpp-1.2.2/docs/guide_xep_0030.rst0000644000175000001440000002143712770302340020276 0ustar mathieuiusers00000000000000XEP-0030: Working with Service Discovery ======================================== XMPP networks can be composed of many individual clients, components, and servers. Determining the JIDs for these entities and the various features they may support is the role of `XEP-0030, Service Discovery `_, or "disco" for short. Every XMPP entity may possess what are called nodes. A node is just a name for some aspect of an XMPP entity. For example, if an XMPP entity provides `Ad-Hoc Commands `_, then it will have a node named ``http://jabber.org/protocol/commands`` which will contain information about the commands provided. Other agents using these ad-hoc commands will interact with the information provided by this node. Note that the node name is just an identifier; there is no inherent meaning. Working with service discovery is about creating and querying these nodes. According to XEP-0030, a node may contain three types of information: identities, features, and items. (Further, extensible, information types are defined in `XEP-0128 `_, but they are not yet implemented by Slixmpp.) Slixmpp provides methods to configure each of these node attributes. Configuring Service Discovery ----------------------------- The design focus for the XEP-0030 plug-in is handling info and items requests in a dynamic fashion, allowing for complex policy decisions of who may receive information and how much, or use alternate backend storage mechanisms for all of the disco data. To do this, each action that the XEP-0030 plug-in performs is handed off to what is called a "node handler," which is just a callback function. These handlers are arranged in a hierarchy that allows for a single handler to manage an entire domain of JIDs (say for a component), while allowing other handler functions to override that global behaviour for certain JIDs, or even further limited to only certain JID and node combinations. The Dynamic Handler Hierarchy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * ``global``: (JID is None, node is None) Handlers assigned at this level for an action (such as ``add_feature``) provide a global default behaviour when the action is performed. * ``jid``: (JID assigned, node is None) At this level, handlers provide a default behaviour for actions affecting any node owned by the JID in question. This level is most useful for component connections; there is effectively no difference between this and the global level when using a client connection. * ``node``: (JID assigned, node assigned) A handler for this level is responsible for carrying out an action for only one node, and is the most specific handler type available. These types of handlers will be most useful for "special" nodes that require special processing different than others provided by the JID, such as using access control lists, or consolidating data from other nodes. Default Static Handlers ~~~~~~~~~~~~~~~~~~~~~~~ The XEP-0030 plug-in provides a default set of handlers that work using in-memory disco stanzas. Each handler simply performs the appropriate lookup or storage operation using these stanzas without doing any complex operations such as checking an ACL, etc. You may find it necessary at some point to revert a particular node or JID to using the default, static handlers. To do so, use the method ``restore_defaults()``. You may also elect to only convert a given set of actions instead. Creating a Node Handler ~~~~~~~~~~~~~~~~~~~~~~~ Every node handler receives three arguments: the JID, the node, and a data parameter that will contain the relevant information for carrying out the handler's action, typically a dictionary. The JID will always have a value, defaulting to ``xmpp.boundjid.full`` for components or ``xmpp.boundjid.bare`` for clients. The node value may be None or a string. Only handlers for the actions ``get_info`` and ``get_items`` need to have return values. For these actions, DiscoInfo or DiscoItems stanzas are exepected as output. It is also acceptable for handlers for these actions to generate an XMPPError exception when necessary. Example Node Handler: +++++++++++++++++++++ Here is one of the built-in default handlers as an example: .. code-block:: python def add_identity(self, jid, node, data): """ Add a new identity to the JID/node combination. The data parameter may provide: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ self.add_node(jid, node) self.nodes[(jid, node)]['info'].add_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) Adding Identities, Features, and Items -------------------------------------- In order to maintain some backwards compatibility, the methods ``add_identity``, ``add_feature``, and ``add_item`` do not follow the method signature pattern of the other API methods (i.e. jid, node, then other options), but rather retain the parameter orders from previous plug-in versions. Adding an Identity ~~~~~~~~~~~~~~~~~~ Adding an identity may be done using either the older positional notation, or with keyword parameters. The example below uses the keyword arguments, but in the same order as expected using positional arguments. .. code-block:: python xmpp['xep_0030'].add_identity(category='client', itype='bot', name='Slixmpp', node='foo', jid=xmpp.boundjid.full, lang='no') The JID and node values determine which handler will be used to perform the ``add_identity`` action. The ``lang`` parameter allows for adding localized versions of identities using the ``xml:lang`` attribute. Adding a Feature ~~~~~~~~~~~~~~~~ The position ordering for ``add_feature()`` is to include the feature, then specify the node and then the JID. The JID and node values determine which handler will be used to perform the ``add_feature`` action. .. code-block:: python xmpp['xep_0030'].add_feature(feature='jabber:x:data', node='foo', jid=xmpp.boundjid.full) Adding an Item ~~~~~~~~~~~~~~ The parameters to ``add_item()`` are potentially confusing due to the fact that adding an item requires two JID and node combinations: the JID and node of the item itself, and the JID and node that will own the item. .. code-block:: python xmpp['xep_0030'].add_item(jid='myitemjid@example.com', name='An Item!', node='owner_node', subnode='item_node', ijid=xmpp.boundjid.full) .. note:: In this case, the owning JID and node are provided with the parameters ``ijid`` and ``node``. Performing Disco Queries ------------------------ The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs and their nodes for disco information. Since these methods are wrappers for sending Iq stanzas, they also accept all of the parameters of the ``Iq.send()`` method. The ``get_items()`` method may also accept the boolean parameter ``iterator``, which when set to ``True`` will return an iterator object using the `XEP-0059 `_ plug-in. .. code-block:: python info = yield from self['xep_0030'].get_info(jid='foo@example.com', node='bar', ifrom='baz@mycomponent.example.com', timeout=30) items = self['xep_0030'].get_info(jid='foo@example.com', node='bar', iterator=True) For more examples on how to use basic disco queries, check the ``disco_browser.py`` example in the ``examples`` directory. Local Queries ~~~~~~~~~~~~~ In some cases, it may be necessary to query the contents of a node owned by the client itself, or one of a component's many JIDs. The same method is used as for normal queries, with two differences. First, the parameter ``local=True`` must be used. Second, the return value will be a DiscoInfo or DiscoItems stanza, not a full Iq stanza. .. code-block:: python info = self['xep_0030'].get_info(node='foo', local=True) items = self['xep_0030'].get_items(jid='somejid@mycomponent.example.com', node='bar', local=True) slixmpp-1.2.2/docs/xeps.rst0000644000175000001440000000357112473141653016651 0ustar mathieuiusers00000000000000Supported XEPS ============== ======= ============================= ================ XEP Description Notes ======= ============================= ================ `0004`_ Data forms `0009`_ Jabber RPC `0012`_ Last Activity `0030`_ Service Discovery `0033`_ Extended Stanza Addressing `0045`_ Multi-User Chat (MUC) Client-side only `0050`_ Ad-hoc Commands `0059`_ Result Set Management `0060`_ Publish/Subscribe (PubSub) Client-side only `0066`_ Out-of-band Data `0078`_ Non-SASL Authentication `0082`_ XMPP Date and Time Profiles `0085`_ Chat-State Notifications `0086`_ Error Condition Mappings `0092`_ Software Version `0128`_ Service Discovery Extensions `0202`_ Entity Time `0203`_ Delayed Delivery `0224`_ Attention `0249`_ Direct MUC Invitations ======= ============================= ================ .. _0004: http://xmpp.org/extensions/xep-0004.html .. _0009: http://xmpp.org/extensions/xep-0009.html .. _0012: http://xmpp.org/extensions/xep-0012.html .. _0030: http://xmpp.org/extensions/xep-0030.html .. _0033: http://xmpp.org/extensions/xep-0033.html .. _0045: http://xmpp.org/extensions/xep-0045.html .. _0050: http://xmpp.org/extensions/xep-0050.html .. _0059: http://xmpp.org/extensions/xep-0059.html .. _0060: http://xmpp.org/extensions/xep-0060.html .. _0066: http://xmpp.org/extensions/xep-0066.html .. _0078: http://xmpp.org/extensions/xep-0078.html .. _0082: http://xmpp.org/extensions/xep-0082.html .. _0085: http://xmpp.org/extensions/xep-0085.html .. _0086: http://xmpp.org/extensions/xep-0086.html .. _0092: http://xmpp.org/extensions/xep-0092.html .. _0128: http://xmpp.org/extensions/xep-0128.html .. _0199: http://xmpp.org/extensions/xep-0199.html .. _0202: http://xmpp.org/extensions/xep-0202.html .. _0203: http://xmpp.org/extensions/xep-0203.html .. _0224: http://xmpp.org/extensions/xep-0224.html .. _0249: http://xmpp.org/extensions/xep-0249.html slixmpp-1.2.2/docs/architecture.rst0000644000175000001440000001261212473170565020354 0ustar mathieuiusers00000000000000.. index:: XMLStream, BaseXMPP, ClientXMPP, ComponentXMPP Slixmpp Architecture ====================== The core of Slixmpp is contained in four classes: ``XMLStream``, ``BaseXMPP``, ``ClientXMPP``, and ``ComponentXMPP``. Along side this stack is a library for working with XML objects that eliminates most of the tedium of creating/manipulating XML. .. image:: _static/images/arch_layers.png :height: 300px :align: center .. index:: XMLStream The Foundation: XMLStream ------------------------- :class:`~slixmpp.xmlstream.xmlstream.XMLStream` is a mostly XMPP-agnostic class whose purpose is to read and write from a bi-directional XML stream. It also allows for callback functions to execute when XML matching given patterns is received; these callbacks are also referred to as :term:`stream handlers `. The class also provides a basic eventing system which can be triggered either manually or on a timed schedule. The event loop ~~~~~~~~~~~~~~ :class:`~slixmpp.xmlstream.xmlstream.XMLStream` instances inherit the :class:`asyncio.BaseProtocol` class, and therefore do not have to handle reads and writes directly, but receive data through :meth:`~slixmpp.xmlstream.xmlstream.XMLStream.data_received` and write data in the socket transport. Upon receiving data, :term:`stream handlers ` are run immediately, except if they are coroutines, in which case they are scheduled using :meth:`asyncio.async`. :term:`Event handlers ` (which are called inside :term:`stream handlers `) work the same way. How XML Text is Turned into Action ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To demonstrate the flow of information, let's consider what happens when this bit of XML is received (with an assumed namespace of ``jabber:client``): .. code-block:: xml Hej! #. **Convert XML strings into objects.** Incoming text is parsed and converted into XML objects (using ElementTree) which are then wrapped into what are referred to as :term:`Stanza objects `. The appropriate class for the new object is determined using a map of namespaced element names to classes. Our incoming XML is thus turned into a :class:`~slixmpp.stanza.Message` :term:`stanza object` because the namespaced element name ``{jabber:client}message`` is associated with the class :class:`~slixmpp.stanza.Message`. #. **Match stanza objects to callbacks.** These objects are then compared against the stored patterns associated with the registered callback handlers. Each handler matching our :term:`stanza object` is then added to a list. #. **Processing callbacks** Every handler in the list is then called with the :term:`stanza object` as a parameter; if the handler is a :class:`~slixmpp.xmlstream.handler.CoroutineCallback` then it will be scheduled in the event loop using :meth:`asyncio.async` instead of run. #. **Raise Custom Events** Since a :term:`stream handler` shouldn't block, if extensive processing for a stanza is required (such as needing to send and receive an :class:`~slixmpp.stanza.Iq` stanza), then custom events must be used. These events are not explicitly tied to the incoming XML stream and may be raised at any time. In contrast to :term:`stream handlers `, these functions are referred to as :term:`event handlers `. The code for :meth:`BaseXMPP._handle_message` follows this pattern, and raises a ``'message'`` event .. code-block:: python self.event('message', msg) #. **Process Custom Events** The :term:`event handlers ` are then executed, passing the stanza as the only argument. .. note:: Events may be raised without needing :term:`stanza objects `. For example, you could use ``self.event('custom', {'a': 'b'})``. You don't even need any arguments: ``self.event('no_parameters')``. However, every event handler MUST accept at least one argument. Finally, after a long trek, our message is handed off to the user's custom handler in order to do awesome stuff:: reply = msg.reply() reply['body'] = "Hey! This is awesome!" reply.send() .. index:: BaseXMPP, XMLStream Raising XMPP Awareness: BaseXMPP -------------------------------- While :class:`~slixmpp.xmlstream.xmlstream.XMLStream` attempts to shy away from anything too XMPP specific, :class:`~slixmpp.basexmpp.BaseXMPP`'s sole purpose is to provide foundational support for sending and receiving XMPP stanzas. This support includes registering the basic message, presence, and iq stanzas, methods for creating and sending stanzas, and default handlers for incoming messages and keeping track of presence notifications. The plugin system for adding new XEP support is also maintained by :class:`~slixmpp.basexmpp.BaseXMPP`. .. index:: ClientXMPP, BaseXMPP ClientXMPP ---------- :class:`~slixmpp.clientxmpp.ClientXMPP` extends :class:`~slixmpp.clientxmpp.BaseXMPP` with additional logic for connecting to an XMPP server by performing DNS lookups. It also adds support for stream features such as STARTTLS and SASL. .. index:: ComponentXMPP, BaseXMPP ComponentXMPP ------------- :class:`~slixmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of :class:`~slixmpp.basexmpp.BaseXMPP` that implements the component handshake protocol. slixmpp-1.2.2/docs/event_index.rst0000644000175000001440000002223012671641065020175 0ustar mathieuiusers00000000000000Event Index =========== .. glossary:: :sorted: connected - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.xmlstream.XMLstream` Signal that a connection has been made with the XMPP server, but a session has not yet been established. connection_failed - **Data:** ``{}`` or ``Failure Stanza`` if available - **Source:** :py:class:`~slixmpp.xmlstream.XMLstream` Signal that a connection can not be established after number of attempts. changed_status - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.roster.item.RosterItem` Triggered when a presence stanza is received from a JID with a show type different than the last presence stanza from the same JID. changed_subscription - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` Triggered whenever a presence stanza with a type of ``subscribe``, ``subscribed``, ``unsubscribe``, or ``unsubscribed`` is received. Note that if the values ``xmpp.auto_authorize`` and ``xmpp.auto_subscribe`` are set to ``True`` or ``False``, and not ``None``, then Slixmpp will either accept or reject all subscription requests before your event handlers are called. Set these values to ``None`` if you wish to make more complex subscription decisions. chatstate_active - **Data:** - **Source:** chatstate_composing - **Data:** - **Source:** chatstate_gone - **Data:** - **Source:** chatstate_inactive - **Data:** - **Source:** chatstate_paused - **Data:** - **Source:** disco_info - **Data:** :py:class:`~slixmpp.plugins.xep_0030.stanza.DiscoInfo` - **Source:** :py:class:`~slixmpp.plugins.xep_0030.disco.xep_0030` Triggered whenever a ``disco#info`` result stanza is received. disco_items - **Data:** :py:class:`~slixmpp.plugins.xep_0030.stanza.DiscoItems` - **Source:** :py:class:`~slixmpp.plugins.xep_0030.disco.xep_0030` Triggered whenever a ``disco#items`` result stanza is received. disconnected - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.xmlstream.XMLstream` Signal that the connection with the XMPP server has been lost. entity_time - **Data:** - **Source:** failed_auth - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.ClientXMPP`, :py:class:`~slixmpp.plugins.xep_0078.xep_0078` Signal that the server has rejected the provided login credentials. gmail_notify - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.plugins.gmail_notify.gmail_notify` Signal that there are unread emails for the Gmail account associated with the current XMPP account. gmail_messages - **Data:** :py:class:`~slixmpp.Iq` - **Source:** :py:class:`~slixmpp.plugins.gmail_notify.gmail_notify` Signal that there are unread emails for the Gmail account associated with the current XMPP account. got_online - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.roster.item.RosterItem` If a presence stanza is received from a JID which was previously marked as offline, and the presence has a show type of '``chat``', '``dnd``', '``away``', or '``xa``', then this event is triggered as well. got_offline - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.roster.item.RosterItem` Signal that an unavailable presence stanza has been received from a JID. groupchat_invite - **Data:** - **Source:** groupchat_direct_invite - **Data:** :py:class:`~slixmpp.Message` - **Source:** :py:class:`~slixmpp.plugins.xep_0249.direct` groupchat_message - **Data:** :py:class:`~slixmpp.Message` - **Source:** :py:class:`~slixmpp.plugins.xep_0045.xep_0045` Triggered whenever a message is received from a multi-user chat room. groupchat_presence - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.plugins.xep_0045.xep_0045` Triggered whenever a presence stanza is received from a user in a multi-user chat room. groupchat_subject - **Data:** :py:class:`~slixmpp.Message` - **Source:** :py:class:`~slixmpp.plugins.xep_0045.xep_0045` Triggered whenever the subject of a multi-user chat room is changed, or announced when joining a room. killed - **Data:** - **Source:** last_activity - **Data:** - **Source:** message - **Data:** :py:class:`~slixmpp.Message` - **Source:** :py:class:`BaseXMPP ` Makes the contents of message stanzas available whenever one is received. Be sure to check the message type in order to handle error messages. message_error - **Data:** :py:class:`~slixmpp.Message` - **Source:** :py:class:`BaseXMPP ` Makes the contents of message stanzas available whenever one is received. Only handler messages with an ``error`` type. message_form - **Data:** :py:class:`~slixmpp.plugins.xep_0004.Form` - **Source:** :py:class:`~slixmpp.plugins.xep_0004.xep_0004` Currently the same as :term:`message_xform`. message_xform - **Data:** :py:class:`~slixmpp.plugins.xep_0004.Form` - **Source:** :py:class:`~slixmpp.plugins.xep_0004.xep_0004` Triggered whenever a data form is received inside a message. muc::[room]::got_offline - **Data:** - **Source:** muc::[room]::got_online - **Data:** - **Source:** muc::[room]::message - **Data:** - **Source:** muc::[room]::presence - **Data:** - **Source:** presence_available - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``available``' is received. presence_error - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``error``' is received. presence_form - **Data:** :py:class:`~slixmpp.plugins.xep_0004.Form` - **Source:** :py:class:`~slixmpp.plugins.xep_0004.xep_0004` This event is present in the XEP-0004 plugin code, but is currently not used. presence_probe - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``probe``' is received. presence_subscribe - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``subscribe``' is received. presence_subscribed - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``subscribed``' is received. presence_unavailable - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``unavailable``' is received. presence_unsubscribe - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``unsubscribe``' is received. presence_unsubscribed - **Data:** :py:class:`~slixmpp.Presence` - **Source:** :py:class:`~slixmpp.BaseXMPP` A presence stanza with a type of '``unsubscribed``' is received. roster_update - **Data:** :py:class:`~slixmpp.stanza.Roster` - **Source:** :py:class:`~slixmpp.ClientXMPP` An IQ result containing roster entries is received. sent_presence - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.roster.multi.Roster` Signal that an initial presence stanza has been written to the XML stream. session_end - **Data:** ``{}`` - **Source:** :py:class:`~slixmpp.xmlstream.XMLstream` Signal that a connection to the XMPP server has been lost and the current stream session has ended. Currently equivalent to :term:`disconnected`, but implementations of `XEP-0198: Stream Management `_ distinguish between the two events. Plugins that maintain session-based state should clear themselves when this event is fired. session_start - **Data:** ``{}`` - **Source:** :py:class:`ClientXMPP `, :py:class:`ComponentXMPP ` :py:class:`XEP-0078 ` Signal that a connection to the XMPP server has been made and a session has been established. socket_error - **Data:** ``Socket`` exception object - **Source:** :py:class:`~slixmpp.xmlstream.XMLstream` stream_error - **Data:** :py:class:`~slixmpp.stanza.StreamError` - **Source:** :py:class:`~slixmpp.BaseXMPP` slixmpp-1.2.2/docs/using_asyncio.rst0000644000175000001440000001053312572154127020540 0ustar mathieuiusers00000000000000.. _using_asyncio: ============= Using asyncio ============= Block on IQ sending ~~~~~~~~~~~~~~~~~~~ :meth:`.Iq.send` now returns a :class:`~.Future` so you can easily block with: .. code-block:: python result = yield from iq.send() .. warning:: If the reply is an IQ with an ``error`` type, this will raise an :class:`.IqError`, and if it timeouts, it will raise an :class:`.IqTimeout`. Don't forget to catch it. You can still use callbacks instead. XEP plugin integration ~~~~~~~~~~~~~~~~~~~~~~ The same changes from the SleekXMPP API apply, so you can do: .. code-block:: python iq_info = yield from self.xmpp['xep_0030'].get_info(jid) But the following will only return a Future: .. code-block:: python iq_info = self.xmpp['xep_0030'].get_info(jid) Callbacks, Event Handlers, and Stream Handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IQ callbacks and :term:`Event Handlers ` can be coroutine functions; in this case, they will be scheduled in the event loop using :meth:`.asyncio.async` and not ran immediately. A :class:`.CoroutineCallback` class has been added as well for :term:`Stream Handlers `, which will use :meth:`.asyncio.async` to schedule the callback. Running the event loop ~~~~~~~~~~~~~~~~~~~~~~ :meth:`.XMLStream.process` is only a thin wrapper on top of ``loop.run_forever()`` (if ``timeout`` is provided then it will only run for this amount of time, and if ``forever`` is False it will run until disconnection). Therefore you can handle the event loop in any way you like instead of using ``process()``. Examples ~~~~~~~~ Blocking until the session is established ----------------------------------------- This code blocks until the XMPP session is fully established, which can be useful to make sure external events aren’t triggering XMPP callbacks while everything is not ready. .. code-block:: python import asyncio, slixmpp client = slixmpp.ClientXMPP('jid@example', 'password') client.connected_event = asyncio.Event() callback = lambda _: client.connected_event.set() client.add_event_handler('session_start', callback) client.connect() loop.run_until_complete(event.wait()) # do some other stuff before running the event loop, e.g. # loop.run_until_complete(httpserver.init()) client.process() Use with other asyncio-based libraries -------------------------------------- This code interfaces with aiohttp to retrieve two pages asynchronously when the session is established, and then send the HTML content inside a simple . .. code-block:: python import asyncio, aiohttp, slixmpp @asyncio.coroutine def get_pythonorg(event): req = yield from aiohttp.request('get', 'http://www.python.org') text = yield from req.text client.send_message(mto='jid2@example', mbody=text) @asyncio.coroutine def get_asyncioorg(event): req = yield from aiohttp.request('get', 'http://www.asyncio.org') text = yield from req.text client.send_message(mto='jid3@example', mbody=text) client = slixmpp.ClientXMPP('jid@example', 'password') client.add_event_handler('session_start', get_pythonorg) client.add_event_handler('session_start', get_asyncioorg) client.connect() client.process() Blocking Iq ----------- This client checks (via XEP-0092) the software used by every entity it receives a message from. After this, it sends a message to a specific JID indicating its findings. .. code-block:: python import asyncio, slixmpp class ExampleClient(slixmpp.ClientXMPP): def __init__(self, *args, **kwargs): slixmpp.ClientXMPP.__init__(self, *args, **kwargs) self.register_plugin('xep_0092') self.add_event_handler('message', self.on_message) @asyncio.coroutine def on_message(self, event): # You should probably handle IqError and IqTimeout exceptions here # but this is an example. version = yield from self['xep_0092'].get_version(message['from']) text = "%s sent me a message, he runs %s" % (message['from'], version['software_version']['name']) self.send_message(mto='master@example.tld', mbody=text) client = ExampleClient('jid@example', 'password') client.connect() client.process() slixmpp-1.2.2/docs/howto/0000755000175000001440000000000013014656513016270 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/howto/stanzas.rst0000644000175000001440000000066112473141654020513 0ustar mathieuiusers00000000000000.. _work-with-stanzas: How to Work with Stanza Objects =============================== .. _create-stanza-interfaces: Defining Stanza Interfaces -------------------------- .. _create-stanza-plugins: Creating Stanza Plugins ----------------------- .. _create-extension-plugins: Creating a Stanza Extension --------------------------- .. _override-parent-interfaces: Overriding a Parent Stanza -------------------------- slixmpp-1.2.2/docs/conf.py0000644000175000001440000001612512603537763016444 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- # # Slixmpp documentation build configuration file, created by # sphinx-quickstart on Tue Aug 9 22:27:06 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Slixmpp' copyright = u'2011, Nathan Fritz, Lance Stout' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.1' # The full version, including alpha/beta/rc tags. release = '1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'tango' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'haiku' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {'headingcolor': '#CFCFCF', 'linkcolor': '#4A7389'} # 00ADEE # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'slixmpp' # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = '%s Documentation' % release # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = { } # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Slixmppdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Slixmpp.tex', u'Slixmpp Documentation', u'Nathan Fritz, Lance Stout', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'slixmpp', u'Slixmpp Documentation', [u'Nathan Fritz, Lance Stout'], 1) ] intersphinx_mapping = {'python': ('http://docs.python.org/3.4', 'python-objects.inv')} slixmpp-1.2.2/docs/getting_started/0000755000175000001440000000000013014656513020317 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/getting_started/proxy.rst0000644000175000001440000000202012473170565022232 0ustar mathieuiusers00000000000000.. _proxy: ========================= Enable HTTP Proxy Support ========================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. In some instances, you may wish to route XMPP traffic through an HTTP proxy, probably to get around restrictive firewalls. Slixmpp provides support for basic HTTP proxying with DIGEST authentication. Enabling proxy support is done in two steps. The first is to instruct Slixmpp to use a proxy, and the second is to configure the proxy details: .. code-block:: python xmpp = ClientXMPP(...) xmpp.use_proxy = True xmpp.proxy_config = { 'host': 'proxy.example.com', 'port': 5555, 'username': 'example_user', 'password': '******' } The ``'username'`` and ``'password'`` fields are optional if the proxy does not require authentication. The Final Product ----------------- .. include:: ../../examples/proxy_echo_client.py :literal: slixmpp-1.2.2/docs/getting_started/iq.rst0000644000175000001440000001362012473141654021467 0ustar mathieuiusers00000000000000Send/Receive IQ Stanzas ======================= Unlike :class:`~slixmpp.stanza.message.Message` and :class:`~slixmpp.stanza.presence.Presence` stanzas which only use text data for basic usage, :class:`~slixmpp.stanza.iq.Iq` stanzas require using XML payloads, and generally entail creating a new Slixmpp plugin to provide the necessary convenience methods to make working with them easier. Basic Use --------- XMPP's use of :class:`~slixmpp.stanza.iq.Iq` stanzas is built around namespaced ```` elements. For clients, just sending the empty ```` element will suffice for retrieving information. For example, a very basic implementation of service discovery would just need to be able to send: .. code-block:: xml Creating Iq Stanzas ~~~~~~~~~~~~~~~~~~~ Slixmpp provides built-in support for creating basic :class:`~slixmpp.stanza.iq.Iq` stanzas this way. The relevant methods are: * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_get` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_set` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_result` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_error` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_query` These methods all follow the same pattern: create or modify an existing :class:`~slixmpp.stanza.iq.Iq` stanza, set the ``'type'`` value based on the method name, and finally add a ```` element with the given namespace. For example, to produce the query above, you would use: .. code-block:: python self.make_iq_get(queryxmlns='http://jabber.org/protocol/disco#info', ito='user@example.com') Sending Iq Stanzas ~~~~~~~~~~~~~~~~~~ Once an :class:`~slixmpp.stanza.iq.Iq` stanza is created, sending it over the wire is done using its :meth:`~slixmpp.stanza.iq.Iq.send()` method, like any other stanza object. However, there are a few extra options to control how to wait for the query's response. These options are: * ``block``: The default behaviour is that :meth:`~slixmpp.stanza.iq.Iq.send()` will block until a response is received and the response stanza will be the return value. Setting ``block`` to ``False`` will cause the call to return immediately. In which case, you will need to arrange some way to capture the response stanza if you need it. * ``timeout``: When using the blocking behaviour, the call will eventually timeout with an error. The default timeout is 30 seconds, but this may be overidden two ways. To change the timeout globally, set: .. code-block:: python self.response_timeout = 10 To change the timeout for a single call, the ``timeout`` parameter works: .. code-block:: python iq.send(timeout=60) * ``callback``: When not using a blocking call, using the ``callback`` argument is a simple way to register a handler that will execute whenever a response is finally received. Using this method, there is no timeout limit. In case you need to remove the callback, the name of the newly created callback is returned. .. code-block:: python cb_name = iq.send(callback=self.a_callback) # ... later if we need to cancel self.remove_handler(cb_name) Properly working with :class:`~slixmpp.stanza.iq.Iq` stanzas requires handling the intended, normal flow, error responses, and timed out requests. To make this easier, two exceptions may be thrown by :meth:`~slixmpp.stanza.iq.Iq.send()`: :exc:`~slixmpp.exceptions.IqError` and :exc:`~slixmpp.exceptions.IqTimeout`. These exceptions only apply to the default, blocking calls. .. code-block:: python try: resp = iq.send() # ... do stuff with expected Iq result except IqError as e: err_resp = e.iq # ... handle error case except IqTimeout: # ... no response received in time pass If you do not care to distinguish between errors and timeouts, then you can combine both cases with a generic :exc:`~slixmpp.exceptions.XMPPError` exception: .. code-block:: python try: resp = iq.send() except XMPPError: # ... Don't care about the response pass Advanced Use ------------ Going beyond the basics provided by Slixmpp requires building at least a rudimentary Slixmpp plugin to create a :term:`stanza object` for interfacting with the :class:`~slixmpp.stanza.iq.Iq` payload. .. seealso:: * :ref:`create-plugin` * :ref:`work-with-stanzas` * :ref:`using-handlers-matchers` The typical way to respond to :class:`~slixmpp.stanza.iq.Iq` requests is to register stream handlers. As an example, suppose we create a stanza class named ``CustomXEP`` which uses the XML element ````, and has a :attr:`~slixmpp.xmlstream.stanzabase.ElementBase.plugin_attrib` value of ``custom_xep``. There are two types of incoming :class:`~slixmpp.stanza.iq.Iq` requests: ``get`` and ``set``. You can register a handler that will accept both and then filter by type as needed, as so: .. code-block:: python self.register_handler(Callback( 'CustomXEP Handler', StanzaPath('iq/custom_xep'), self._handle_custom_iq)) # ... def _handle_custom_iq(self, iq): if iq['type'] == 'get': # ... pass elif iq['type'] == 'set': # ... pass else: # ... This will capture error responses too pass If you want to filter out query types beforehand, you can adjust the matching filter by using ``@type=get`` or ``@type=set`` if you are using the recommended :class:`~slixmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher. .. code-block:: python self.register_handler(Callback( 'CustomXEP Handler', StanzaPath('iq@type=get/custom_xep'), self._handle_custom_iq_get)) # ... def _handle_custom_iq_get(self, iq): assert(iq['type'] == 'get') slixmpp-1.2.2/docs/getting_started/echobot.rst0000644000175000001440000003323012773535615022507 0ustar mathieuiusers00000000000000.. _echobot: =============================== Slixmpp Quickstart - Echo Bot =============================== .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version with `Git `_. As a basic starting project, we will create an echo bot which will reply to any messages sent to it. We will also go through adding some basic command line configuration for enabling or disabling debug log outputs and setting the username and password for the bot. For the command line options processing, we will use the built-in ``optparse`` module and the ``getpass`` module for reading in passwords. TL;DR Just Give Me the Code --------------------------- As you wish: :ref:`the completed example `. Overview -------- To get started, here is a brief outline of the structure that the final project will have: .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import sys import asyncio import logging import getpass from optparse import OptionParser import slixmpp '''Here we will create out echo bot class''' if __name__ == '__main__': '''Here we will configure and read command line options''' '''Here we will instantiate our echo bot''' '''Finally, we connect the bot and start listening for messages''' Creating the EchoBot Class -------------------------- There are three main types of entities within XMPP — servers, components, and clients. Since our echo bot will only be responding to a few people, and won't need to remember thousands of users, we will use a client connection. A client connection is the same type that you use with your standard IM client such as Pidgin or Psi. Slixmpp comes with a :class:`ClientXMPP ` class which we can extend to add our message echoing feature. :class:`ClientXMPP ` requires the parameters ``jid`` and ``password``, so we will let our ``EchoBot`` class accept those as well. .. code-block:: python class EchoBot(slixmpp.ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) Handling Session Start ~~~~~~~~~~~~~~~~~~~~~~ The XMPP spec requires clients to broadcast its presence and retrieve its roster (buddy list) once it connects and establishes a session with the XMPP server. Until these two tasks are completed, some servers may not deliver or send messages or presence notifications to the client. So we now need to be sure that we retrieve our roster and send an initial presence once the session has started. To do that, we will register an event handler for the :term:`session_start` event. .. code-block:: python def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.start) Since we want the method ``self.start`` to execute when the :term:`session_start` event is triggered, we also need to define the ``self.start`` handler. .. code-block:: python def start(self, event): self.send_presence() self.get_roster() .. warning:: Not sending an initial presence and retrieving the roster when using a client instance can prevent your program from receiving presence notifications or messages depending on the XMPP server you have chosen. Our event handler, like every event handler, accepts a single parameter which typically is the stanza that was received that caused the event. In this case, ``event`` will just be an empty dictionary since there is no associated data. Our first task of sending an initial presence is done using :meth:`send_presence `. Calling :meth:`send_presence ` without any arguments will send the simplest stanza allowed in XMPP: .. code-block:: xml The second requirement is fulfilled using :meth:`get_roster `, which will send an IQ stanza requesting the roster to the server and then wait for the response. You may be wondering what :meth:`get_roster ` returns since we are not saving any return value. The roster data is saved by an internal handler to ``self.roster``, and in the case of a :class:`ClientXMPP ` instance to ``self.client_roster``. (The difference between ``self.roster`` and ``self.client_roster`` is that ``self.roster`` supports storing roster information for multiple JIDs, which is useful for components, whereas ``self.client_roster`` stores roster data for just the client's JID.) It is possible for a timeout to occur while waiting for the server to respond, which can happen if the network is excessively slow or the server is no longer responding. In that case, an :class:`IQTimeout ` is raised. Similarly, an :class:`IQError ` exception can be raised if the request contained bad data or requested the roster for the wrong user. In either case, you can wrap the ``get_roster()`` call in a ``try``/``except`` block to retry the roster retrieval process. The XMPP stanzas from the roster retrieval process could look like this: .. code-block:: xml Responding to Messages ~~~~~~~~~~~~~~~~~~~~~~ Now that an ``EchoBot`` instance handles :term:`session_start`, we can begin receiving and responding to messages. Now we can register a handler for the :term:`message` event that is raised whenever a messsage is received. .. code-block:: python def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.start) self.add_event_handler('message', self.message) The :term:`message` event is fired whenever a ```` stanza is received, including for group chat messages, errors, etc. Properly responding to messages thus requires checking the ``'type'`` interface of the message :term:`stanza object`. For responding to only messages addressed to our bot (and not from a chat room), we check that the type is either ``normal`` or ``chat``. (Other potential types are ``error``, ``headline``, and ``groupchat``.) .. code-block:: python def message(self, msg): if msg['type'] in ('normal', 'chat'): msg.reply("Thanks for sending:\n%s" % msg['body']).send() Let's take a closer look at the ``.reply()`` method used above. For message stanzas, ``.reply()`` accepts the parameter ``body`` (also as the first positional argument), which is then used as the value of the ```` element of the message. Setting the appropriate ``to`` JID is also handled by ``.reply()``. Another way to have sent the reply message would be to use :meth:`send_message `, which is a convenience method for generating and sending a message based on the values passed to it. If we were to use this method, the above code would look as so: .. code-block:: python def message(self, msg): if msg['type'] in ('normal', 'chat'): self.send_message(mto=msg['from'], mbody='Thanks for sending:\n%s' % msg['body']) Whichever method you choose to use, the results in action will look like this: .. code-block:: xml Hej! Thanks for sending: Hej! .. note:: XMPP does not require stanzas sent by a client to include a ``from`` attribute, and leaves that responsibility to the XMPP server. However, if a sent stanza does include a ``from`` attribute, it must match the full JID of the client or some servers will reject it. Slixmpp thus leaves out the ``from`` attribute when replying using a client connection. Command Line Arguments and Logging ---------------------------------- While this isn't part of Slixmpp itself, we do want our echo bot program to be able to accept a JID and password from the command line instead of hard coding them. We will use the ``optparse`` module for this, though there are several alternative methods, including the newer ``argparse`` module. We want to accept three parameters: the JID for the echo bot, its password, and a flag for displaying the debugging logs. We also want these to be optional parameters, since passing a password directly through the command line can be a security risk. .. code-block:: python if __name__ == '__main__': optp = OptionParser() optp.add_option('-d', '--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) 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() if opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") Since we included a flag for enabling debugging logs, we need to configure the ``logging`` module to behave accordingly. .. code-block:: python if __name__ == '__main__': # .. option parsing from above .. logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') Connecting to the Server and Processing --------------------------------------- There are three steps remaining until our echo bot is complete: 1. We need to instantiate the bot. 2. The bot needs to connect to an XMPP server. 3. We have to instruct the bot to start running and processing messages. Creating the bot is straightforward, but we can also perform some configuration at this stage. For example, let's say we want our bot to support `service discovery `_ and `pings `_: .. code-block:: python if __name__ == '__main__': # .. option parsing and logging steps from above xmpp = EchoBot(opts.jid, opts.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0199') # Ping If the ``EchoBot`` class had a hard dependency on a plugin, we could register that plugin in the ``EchoBot.__init__`` method instead. .. note:: If you are using the OpenFire server, you will need to include an additional configuration step. OpenFire supports a different version of SSL than what most servers and Slixmpp support. .. code-block:: python import ssl xmpp.ssl_version = ssl.PROTOCOL_SSLv3 Now we're ready to connect and begin echoing messages. If you have the package ``aiodns`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP` method will perform a DNS query to find the appropriate server to connect to for the given JID. If you do not have ``aiodns``, then Slixmpp will attempt to connect to the hostname used by the JID, unless an address tuple is supplied to :meth:`slixmpp.clientxmpp.ClientXMPP`. .. code-block:: python if __name__ == '__main__': # .. option parsing & echo bot configuration if xmpp.connect(): xmpp.process(block=True) else: print('Unable to connect') To begin responding to messages, you'll see we called :meth:`slixmpp.basexmpp.BaseXMPP.process` which will start the event handling, send queue, and XML reader threads. It will also call the :meth:`slixmpp.plugins.base.BasePlugin.post_init` method on all registered plugins. By passing ``block=True`` to :meth:`slixmpp.basexmpp.BaseXMPP.process` we are running the main processing loop in the main thread of execution. The :meth:`slixmpp.basexmpp.BaseXMPP.process` call will not return until after Slixmpp disconnects. If you need to run the client in the background for another program, use ``block=False`` to spawn the processing loop in its own thread. .. note:: Before 1.0, controlling the blocking behaviour of :meth:`slixmpp.basexmpp.BaseXMPP.process` was done via the ``threaded`` argument. This arrangement was a source of confusion because some users interpreted that as controlling whether or not Slixmpp used threads at all, instead of how the processing loop itself was spawned. The statements ``xmpp.process(threaded=False)`` and ``xmpp.process(block=True)`` are equivalent. .. _echobot_complete: The Final Product ----------------- Here then is what the final result should look like after working through the guide above. The code can also be found in the Slixmpp `examples directory `_. .. compound:: You can run the code using: .. code-block:: sh python echobot.py -d -j echobot@example.com which will prompt for the password and then begin echoing messages. To test, open your regular IM client and start a chat with the echo bot. Messages you send to it should be mirrored back to you. Be careful if you are using the same JID for the echo bot that you also have logged in with another IM client. Messages could be routed to your IM client instead of the bot. .. include:: ../../examples/echo_client.py :literal: slixmpp-1.2.2/docs/getting_started/sendlogout.rst0000644000175000001440000000702412773536031023242 0ustar mathieuiusers00000000000000Sign in, Send a Message, and Disconnect ======================================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. A common use case for Slixmpp is to send one-off messages from 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 ` 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 slixmpp class SendMsgBot(slixmpp.ClientXMPP): def __init__(self, jid, password, recipient, msg): super().__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 `. .. 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 `. Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call :meth:`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) `. .. 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) ` 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: slixmpp-1.2.2/docs/getting_started/component.rst0000644000175000001440000000431012473170565023057 0ustar mathieuiusers00000000000000.. _echocomponent: ================================= Create and Run a Server Component ================================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version with `Git `_. Many XMPP applications eventually graduate to requiring to run as a server component in order to meet scalability requirements. To demonstrate how to turn an XMPP client bot into a component, we'll turn the echobot example (:ref:`echobot`) into a component version. The first difference is that we will add an additional import statement: .. code-block:: python from slixmpp.componentxmpp import ComponentXMPP Likewise, we will change the bot's class definition to match: .. code-block:: python class EchoComponent(ComponentXMPP): def __init__(self, jid, secret, server, port): ComponentXMPP.__init__(self, jid, secret, server, port) A component instance requires two extra parameters compared to a client instance: ``server`` and ``port``. These specifiy the name and port of the XMPP server that will be accepting the component. For example, for a MUC component, the following could be used: .. code-block:: python muc = ComponentXMPP('muc.slixmpp.com', '******', 'slixmpp.com', 5555) .. note:: The ``server`` value is **NOT** derived from the provided JID for the component, unlike with client connections. One difference with the component version is that we do not have to handle the :term:`session_start` event if we don't wish to deal with presence. The other, main difference with components is that the ``'from'`` value for every stanza must be explicitly set, since components may send stanzas from multiple JIDs. To do so, the :meth:`~slixmpp.basexmpp.BaseXMPP.send_message()` and :meth:`~slixmpp.basexmpp.BaseXMPP.send_presence()` accept the parameters ``mfrom`` and ``pfrom``, respectively. For any method that uses :class:`~slixmpp.stanza.iq.Iq` stanzas, ``ifrom`` may be used. Final Product ------------- .. include:: ../../examples/echo_component.py :literal: slixmpp-1.2.2/docs/getting_started/presence.rst0000644000175000001440000000007412473141654022661 0ustar mathieuiusers00000000000000Manage Presence Subscriptions ============================= slixmpp-1.2.2/docs/getting_started/scheduler.rst0000644000175000001440000000007612473141654023035 0ustar mathieuiusers00000000000000Send a Message Every 5 Minutes ============================== slixmpp-1.2.2/docs/getting_started/muc.rst0000644000175000001440000001527612770302340021642 0ustar mathieuiusers00000000000000.. _mucbot: ========================= Mulit-User Chat (MUC) Bot ========================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version from `Git `_. Now that you've got the basic gist of using Slixmpp by following the echobot example (:ref:`echobot`), we can use one of the bundled plugins to create a very popular XMPP starter project: a `Multi-User Chat`_ (MUC) bot. Our bot will login to an XMPP server, join an MUC chat room and "lurk" indefinitely, responding with a generic message to anyone that mentions its nickname. It will also greet members as they join the chat room. .. _`multi-user chat`: http://xmpp.org/extensions/xep-0045.html Joining The Room ---------------- As usual, our code will be based on the pattern explained in :ref:`echobot`. To start, we create an ``MUCBot`` class based on :class:`ClientXMPP ` and which accepts parameters for the JID of the MUC room to join, and the nick that the bot will use inside the chat room. We also register an :term:`event handler` for the :term:`session_start` event. .. code-block:: python import slixmpp class MUCBot(slixmpp.ClientXMPP): def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) After initialization, we also need to register the MUC (XEP-0045) plugin so that we can make use of the group chat plugin's methods and events. .. code-block:: python xmpp.register_plugin('xep_0045') Finally, we can make our bot join the chat room once an XMPP session has been established: .. code-block:: python def start(self, event): self.get_roster() self.send_presence() self.plugin['xep_0045'].join_muc(self.room, self.nick, wait=True) Note that as in :ref:`echobot`, we need to include send an initial presence and request the roster. Next, we want to join the group chat, so we call the ``join_muc`` method of the MUC plugin. .. note:: The :attr:`plugin ` attribute is dictionary that maps to instances of plugins that we have previously registered, by their names. Adding Functionality -------------------- Currently, our bot just sits dormantly inside the chat room, but we would like it to respond to two distinct events by issuing a generic message in each case to the chat room. In particular, when a member mentions the bot's nickname inside the chat room, and when a member joins the chat room. Responding to Mentions ~~~~~~~~~~~~~~~~~~~~~~ Whenever a user mentions our bot's nickname in chat, our bot will respond with a generic message resembling *"I heard that, user."* We do this by examining all of the messages sent inside the chat and looking for the ones which contain the nickname string. First, we register an event handler for the :term:`groupchat_message` event inside the bot's ``__init__`` function. .. note:: We do not register a handler for the :term:`message` event in this bot, but if we did, the group chat message would have been sent to both handlers. .. code-block:: python def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) self.add_event_handler("groupchat_message", self.muc_message) Then, we can send our generic message whenever the bot's nickname gets mentioned. .. warning:: Always check that a message is not from yourself, otherwise you will create an infinite loop responding to your own messages. .. code-block:: python def muc_message(self, msg): if msg['mucnick'] != self.nick and self.nick in msg['body']: self.send_message(mto=msg['from'].bare, mbody="I heard that, %s." % msg['mucnick'], mtype='groupchat') Greeting Members ~~~~~~~~~~~~~~~~ Now we want to greet member whenever they join the group chat. To do this we will use the dynamic ``muc::room@server::got_online`` [1]_ event so it's a good idea to register an event handler for it. .. note:: The groupchat_presence event is triggered whenever a presence stanza is received from any chat room, including any presences you send yourself. To limit event handling to a single room, use the events ``muc::room@server::presence``, ``muc::room@server::got_online``, or ``muc::room@server::got_offline``. .. code-block:: python def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) self.add_event_handler("groupchat_message", self.muc_message) self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) Now all that's left to do is to greet them: .. code-block:: python def muc_online(self, presence): if presence['muc']['nick'] != self.nick: self.send_message(mto=presence['from'].bare, mbody="Hello, %s %s" % (presence['muc']['role'], presence['muc']['nick']), mtype='groupchat') .. [1] this is similar to the :term:`got_online` event and is sent by the xep_0045 plugin whenever a member joins the referenced MUC chat room. Final Product ------------- .. compound:: The final step is to create a small runner script for initialising our ``MUCBot`` 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 muc.py -d -j jid@example.com -r room@muc.example.net -n lurkbot which will prompt for the password, log in, and join the group chat. To test, open your regular IM client and join the same group chat that you sent the bot to. You will see ``lurkbot`` as one of the members in the group chat, and that it greeted you upon entry. Send a message with the string "lurkbot" inside the body text, and you will also see that it responds with our pre-programmed customized message. .. include:: ../../examples/muc.py :literal: slixmpp-1.2.2/docs/make.bat0000644000175000001440000001064112473141654016542 0ustar mathieuiusers00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Slixmpp.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Slixmpp.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end slixmpp-1.2.2/docs/plugin_arch.rst0000644000175000001440000000005012473141654020153 0ustar mathieuiusers00000000000000Plugin Architecture =================== slixmpp-1.2.2/docs/license.rst0000644000175000001440000000010212473141654017300 0ustar mathieuiusers00000000000000.. _license: License (MIT) ============= .. include:: ../LICENSE slixmpp-1.2.2/docs/api/0000755000175000001440000000000013014656513015701 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/api/componentxmpp.rst0000644000175000001440000000017112473141654021344 0ustar mathieuiusers00000000000000============= ComponentXMPP ============= .. module:: slixmpp.componentxmpp .. autoclass:: ComponentXMPP :members: slixmpp-1.2.2/docs/api/clientxmpp.rst0000644000175000001440000000015212473141654020617 0ustar mathieuiusers00000000000000========== ClientXMPP ========== .. module:: slixmpp.clientxmpp .. autoclass:: ClientXMPP :members: slixmpp-1.2.2/docs/api/basexmpp.rst0000644000175000001440000000014012473141654020250 0ustar mathieuiusers00000000000000======== BaseXMPP ======== .. module:: slixmpp.basexmpp .. autoclass:: BaseXMPP :members: slixmpp-1.2.2/docs/api/exceptions.rst0000644000175000001440000000027112473141654020617 0ustar mathieuiusers00000000000000Exceptions ========== .. module:: slixmpp.exceptions .. autoexception:: XMPPError :members: .. autoexception:: IqError :members: .. autoexception:: IqTimeout :members: slixmpp-1.2.2/docs/api/stanza/0000755000175000001440000000000013014656513017201 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/api/stanza/message.rst0000644000175000001440000000014012473170565021360 0ustar mathieuiusers00000000000000Message Stanza ============== .. module:: slixmpp.stanza .. autoclass:: Message :members: slixmpp-1.2.2/docs/api/stanza/iq.rst0000644000175000001440000000012212473170565020345 0ustar mathieuiusers00000000000000IQ Stanza ========= .. module:: slixmpp.stanza .. autoclass:: Iq :members: slixmpp-1.2.2/docs/api/stanza/rootstanza.rst0000644000175000001440000000015112473170565022142 0ustar mathieuiusers00000000000000Root Stanza =========== .. module:: slixmpp.stanza.rootstanza .. autoclass:: RootStanza :members: slixmpp-1.2.2/docs/api/stanza/presence.rst0000644000175000001440000000014412473170565021544 0ustar mathieuiusers00000000000000Presence Stanza =============== .. module:: slixmpp.stanza .. autoclass:: Presence :members: slixmpp-1.2.2/docs/api/xmlstream/0000755000175000001440000000000013014656513017715 5ustar mathieuiusers00000000000000slixmpp-1.2.2/docs/api/xmlstream/stanzabase.rst0000644000175000001440000001040012473170565022603 0ustar mathieuiusers00000000000000.. _stanzabase: ============== Stanza Objects ============== .. module:: slixmpp.xmlstream.stanzabase The :mod:`~slixmpp.xmlstream.stanzabase` module provides a wrapper for the standard :mod:`~xml.etree.ElementTree` module that makes working with XML less painful. Instead of having to manually move up and down an element tree and insert subelements and attributes, you can interact with an object that behaves like a normal dictionary or JSON object, which silently maps keys to XML attributes and elements behind the scenes. Overview -------- The usefulness of this layer grows as the XML you have to work with becomes nested. The base unit here, :class:`ElementBase`, can map to a single XML element, or several depending on how advanced of a mapping is desired from interface keys to XML structures. For example, a single :class:`ElementBase` derived class could easily describe: .. code-block:: xml Hi! Custom item 1 Custom item 2 Custom item 3 If that chunk of XML were put in the :class:`ElementBase` instance ``msg``, we could extract the data from the XML using:: >>> msg['extra'] ['Custom item 1', 'Custom item 2', 'Custom item 3'] Provided we set up the handler for the ``'extra'`` interface to load the ```` element content into a list. The key concept is that given an XML structure that will be repeatedly used, we can define a set of :term:`interfaces` which when we read from, write to, or delete, will automatically manipulate the underlying XML as needed. In addition, some of these interfaces may in turn reference child objects which expose interfaces for particularly complex child elements of the original XML chunk. .. seealso:: :ref:`create-stanza-interfaces`. Because the :mod:`~slixmpp.xmlstream.stanzabase` module was developed as part of an `XMPP `_ library, these chunks of XML are referred to as :term:`stanzas `, and in Slixmpp we refer to a subclass of :class:`ElementBase` which defines the interfaces needed for interacting with a given :term:`stanza` a :term:`stanza object`. To make dealing with more complicated and nested :term:`stanzas ` or XML chunks easier, :term:`stanza objects ` can be composed in two ways: as iterable child objects or as plugins. Iterable child stanzas, or :term:`substanzas `, are accessible through a special ``'substanzas'`` interface. This option is useful for stanzas which may contain more than one of the same kind of element. When there is only one child element, the plugin method is more useful. For plugins, a parent stanza object delegates one of its XML child elements to the plugin stanza object. Here is an example: .. code-block:: xml We can can arrange this stanza into two objects: an outer, wrapper object for dealing with the ```` element and its attributes, and a plugin object to control the ```` payload element. If we give the plugin object the name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then we can access the plugin as so:: >>> iq['disco_info'] ' ' We can then drill down through the plugin object's interfaces as desired:: >>> iq['disco_info']['identities'] [('client', 'bot', 'Slixmpp Bot')] Plugins may also add new interfaces to the parent stanza object as if they had been defined by the parent directly, and can also override the behaviour of an interface defined by the parent. .. seealso:: - :ref:`create-stanza-plugins` - :ref:`create-extension-plugins` - :ref:`override-parent-interfaces` Registering Stanza Plugins -------------------------- .. autofunction:: register_stanza_plugin ElementBase ----------- .. autoclass:: ElementBase :members: :private-members: :special-members: StanzaBase ---------- .. autoclass:: StanzaBase :members: slixmpp-1.2.2/docs/api/xmlstream/matcher.rst0000644000175000001440000000113112473141654022071 0ustar mathieuiusers00000000000000Stanza Matchers =============== The Basic Matcher ----------------- .. module:: slixmpp.xmlstream.matcher.base .. autoclass:: MatcherBase :members: ID Matching ----------- .. module:: slixmpp.xmlstream.matcher.id .. autoclass:: MatcherId :members: Stanza Path Matching -------------------- .. module:: slixmpp.xmlstream.matcher.stanzapath .. autoclass:: StanzaPath :members: XPath ----- .. module:: slixmpp.xmlstream.matcher.xpath .. autoclass:: MatchXPath :members: XMLMask ------- .. module:: slixmpp.xmlstream.matcher.xmlmask .. autoclass:: MatchXMLMask :members: slixmpp-1.2.2/docs/api/xmlstream/tostring.rst0000644000175000001440000000356212473170565022334 0ustar mathieuiusers00000000000000.. module:: slixmpp.xmlstream.tostring .. _tostring: XML Serialization ================= Since the XML layer of Slixmpp is based on :mod:`~xml.etree.ElementTree`, why not just use the built-in :func:`~xml.etree.ElementTree.tostring` method? The answer is that using that method produces ugly results when using namespaces. The :func:`tostring()` method used here intelligently hides namespaces when able and does not introduce excessive namespace prefixes:: >>> from slixmpp.xmlstream.tostring import tostring >>> from xml.etree import cElementTree as ET >>> xml = ET.fromstring('') >>> ET.tostring(xml) '' >>> tostring(xml) '' As a side effect of this namespace hiding, using :func:`tostring()` may produce unexpected results depending on how the :func:`tostring()` method is invoked. For example, when sending XML on the wire, the main XMPP stanzas with their namespace of ``jabber:client`` will not include the namespace because that is already declared by the stream header. But, if you create a :class:`~slixmpp.stanza.message.Message` instance and dump it to the terminal, the ``jabber:client`` namespace will appear. .. autofunction:: slixmpp.xmlstream.tostring Escaping Special Characters --------------------------- In order to prevent errors when sending arbitrary text as the textual content of an XML element, certain characters must be escaped. These are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping mechanism is to replace those characters with their equivalent escape entities: ``&``, ``<``, ``>``, ``'``, and ``"``. In the future, the use of CDATA sections may be allowed to reduce the size of escaped text or for when other XMPP processing agents do not undertand these entities. .. autofunction:: xml_escape slixmpp-1.2.2/docs/api/xmlstream/xmlstream.rst0000644000175000001440000000016212473141654022465 0ustar mathieuiusers00000000000000========== XML Stream ========== .. module:: slixmpp.xmlstream.xmlstream .. autoclass:: XMLStream :members: slixmpp-1.2.2/docs/api/xmlstream/jid.rst0000644000175000001440000000013612473170565021223 0ustar mathieuiusers00000000000000Jabber IDs (JID) ================= .. module:: slixmpp.jid .. autoclass:: JID :members: slixmpp-1.2.2/docs/api/xmlstream/handler.rst0000644000175000001440000000060312473170565022071 0ustar mathieuiusers00000000000000Stanza Handlers =============== The Basic Handler ----------------- .. module:: slixmpp.xmlstream.handler.base .. autoclass:: BaseHandler :members: Callback -------- .. module:: slixmpp.xmlstream.handler .. autoclass:: Callback :members: CoroutineCallback ----------------- .. autoclass:: CoroutineCallback :members: Waiter ------ .. autoclass:: Waiter :members: slixmpp-1.2.2/docs/glossary.rst0000644000175000001440000000226312473170565017536 0ustar mathieuiusers00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: stream handler A callback function that accepts stanza objects pulled directly from the XML stream. A stream handler is encapsulated in a object that includes a :class:`Matcher <.MatcherBase>` object, and which provides additional semantics. For example, the :class:`.Waiter` handler wrapper blocks thread execution until a matching stanza is received. event handler A callback function that responds to events raised by :meth:`.XMLStream.event`. stanza object Informally may refer both to classes which extend :class:`.ElementBase` or :class:`.StanzaBase`, and to objects of such classes. A stanza object is a wrapper for an XML object which exposes :class:`dict` like interfaces which may be assigned to, read from, or deleted. stanza plugin A :term:`stanza object` which has been registered as a potential child of another stanza object. The plugin stanza may accessed through the parent stanza using the plugin's ``plugin_attrib`` as an interface. substanza See :term:`stanza plugin` slixmpp-1.2.2/MANIFEST.in0000644000175000001440000000032712572154127015742 0ustar mathieuiusers00000000000000include README.rst include LICENSE include run_tests.py include slixmpp/stringprep.pyx recursive-include docs Makefile *.bat *.py *.rst *.css *.ttf *.png recursive-include examples *.py recursive-include tests *.py slixmpp-1.2.2/tests/0000755000175000001440000000000013014656513015342 5ustar mathieuiusers00000000000000slixmpp-1.2.2/tests/test_stream_xep_0092.py0000644000175000001440000000356212467075620021607 0ustar mathieuiusers00000000000000import threading import unittest from slixmpp.test import SlixTest class TestStreamSet(SlixTest): def tearDown(self): self.stream_close() def testHandleSoftwareVersionRequest(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) self.xmpp['xep_0092'].name = 'Slixmpp' self.xmpp['xep_0092'].version = 'dev' self.xmpp['xep_0092'].os = 'Linux' self.recv(""" """) self.send(""" Slixmpp dev Linux """) def testMakeSoftwareVersionRequest(self): results = [] def callback(result): results.append((result['software_version']['name'], result['software_version']['version'], result['software_version']['os'])) self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) self.xmpp['xep_0092'].get_version('foo@bar', callback=callback) self.send(""" """) self.recv(""" Foo 1.0 Linux """) expected = [('Foo', '1.0', 'Linux')] self.assertEqual(results, expected, "Did not receive expected results: %s" % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet) slixmpp-1.2.2/tests/test_stream_xep_0128.py0000644000175000001440000000713312770302340021572 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest class TestStreamExtendedDisco(SlixTest): """ Test using the XEP-0128 plugin. """ def tearDown(self): self.stream_close() def testUsingExtendedInfo(self): self.stream_start(mode='client', jid='tester@localhost', plugins=['xep_0030', 'xep_0004', 'xep_0128']) form = self.xmpp['xep_0004'].make_form(ftype='result') form.addField(var='FORM_TYPE', ftype='hidden', value='testing') info_ns = 'http://jabber.org/protocol/disco#info' self.xmpp['xep_0030'].add_identity(node='test', category='client', itype='bot') self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) self.xmpp['xep_0128'].set_extended_info(node='test', data=form) self.recv(""" """) self.send(""" testing """) def testUsingMultipleExtendedInfo(self): self.stream_start(mode='client', jid='tester@localhost', plugins=['xep_0030', 'xep_0004', 'xep_0128']) form1 = self.xmpp['xep_0004'].make_form(ftype='result') form1.addField(var='FORM_TYPE', ftype='hidden', value='testing') form2 = self.xmpp['xep_0004'].make_form(ftype='result') form2.addField(var='FORM_TYPE', ftype='hidden', value='testing_2') info_ns = 'http://jabber.org/protocol/disco#info' self.xmpp['xep_0030'].add_identity(node='test', category='client', itype='bot') self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) self.xmpp['xep_0128'].set_extended_info(node='test', data=[form1, form2]) self.recv(""" """) self.send(""" testing testing_2 """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExtendedDisco) slixmpp-1.2.2/tests/test_stanza_iq.py0000644000175000001440000000453612466756332020766 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream.stanzabase import ET class TestIqStanzas(SlixTest): def tearDown(self): """Shutdown the XML stream after testing.""" self.stream_close() def testSetup(self): """Test initializing default Iq values.""" iq = self.Iq() self.check(iq, """ """) def testPayload(self): """Test setting Iq stanza payload.""" iq = self.Iq() iq.set_payload(ET.Element('{test}tester')) self.check(iq, """ """, use_values=False) def testUnhandled(self): """Test behavior for Iq.unhandled.""" self.stream_start() self.recv(""" """) iq = self.Iq() iq['id'] = 'test' iq['error']['condition'] = 'feature-not-implemented' iq['error']['text'] = 'No handlers registered for this request.' self.send(iq, """ No handlers registered for this request. """) def testQuery(self): """Test modifying query element of Iq stanzas.""" iq = self.Iq() iq['query'] = 'query_ns' self.check(iq, """ """) iq['query'] = 'query_ns2' self.check(iq, """ """) self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") del iq['query'] self.check(iq, """ """) def testReply(self): """Test setting proper result type in Iq replies.""" iq = self.Iq() iq['to'] = 'user@localhost' iq['type'] = 'get' iq = iq.reply() self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas) slixmpp-1.2.2/tests/test_stanza_xep_0184.py0000644000175000001440000000174112466746741021623 0ustar mathieuiusers00000000000000import unittest from slixmpp import Message from slixmpp.test import SlixTest import slixmpp.plugins.xep_0184 as xep_0184 from slixmpp.xmlstream import register_stanza_plugin class TestReciept(SlixTest): def setUp(self): register_stanza_plugin(Message, xep_0184.Request) register_stanza_plugin(Message, xep_0184.Received) def testCreateRequest(self): request = """ """ msg = self.Message() self.assertEqual(msg['request_receipt'], False) msg['request_receipt'] = True self.check(msg, request) def testCreateReceived(self): received = """ """ msg = self.Message() msg['receipt'] = '1' self.check(msg, received) suite = unittest.TestLoader().loadTestsFromTestCase(TestReciept) slixmpp-1.2.2/tests/test_stanza_xep_0033.py0000644000175000001440000000742512770302340021576 0ustar mathieuiusers00000000000000import unittest from slixmpp import Message from slixmpp.test import SlixTest import slixmpp.plugins.xep_0033 as xep_0033 from slixmpp.xmlstream import register_stanza_plugin class TestAddresses(SlixTest): def setUp(self): register_stanza_plugin(Message, xep_0033.Addresses) def testAddAddress(self): """Testing adding extended stanza address.""" msg = self.Message() msg['addresses'].add_address(atype='to', jid='to@header1.org') self.check(msg, """
""") msg = self.Message() msg['addresses'].add_address(atype='replyto', jid='replyto@header1.org', desc='Reply address') self.check(msg, """
""") def testAddAddresses(self): """Testing adding multiple extended stanza addresses.""" xmlstring = """
""" msg = self.Message() msg['addresses'].set_addresses([ {'type':'replyto', 'jid':'replyto@header1.org', 'desc':'Reply address'}, {'type':'cc', 'jid':'cc@header2.org'}, {'type':'bcc', 'jid':'bcc@header2.org'}]) self.check(msg, xmlstring) msg = self.Message() msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', 'desc':'Reply address'}] msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] self.check(msg, xmlstring) def testAddURI(self): """Testing adding URI attribute to extended stanza address.""" msg = self.Message() addr = msg['addresses'].add_address(atype='to', jid='to@header1.org', node='foo') self.check(msg, """
""") addr['uri'] = 'mailto:to@header2.org' self.check(msg, """
""") def testDelivered(self): """Testing delivered attribute of extended stanza addresses.""" xmlstring = """
""" msg = self.Message() addr = msg['addresses'].add_address(jid='to@header1.org', atype='to') self.check(msg, xmlstring % '') addr['delivered'] = True self.check(msg, xmlstring % 'delivered="true"') addr['delivered'] = False self.check(msg, xmlstring % '') suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) slixmpp-1.2.2/tests/test_stanza_xep_0047.py0000644000175000001440000000500312466750407021606 0ustar mathieuiusers00000000000000import unittest from slixmpp.exceptions import XMPPError from slixmpp import Iq from slixmpp.test import SlixTest from slixmpp.plugins.xep_0047 import Data from slixmpp.xmlstream import register_stanza_plugin, ET class TestIBB(SlixTest): def setUp(self): register_stanza_plugin(Iq, Data) def testInvalidBase64MidEqual(self): """ Test detecting invalid base64 data with = inside the character data instead of at the end. """ iq = Iq(xml=ET.fromstring(""" ABC=DEFGH """)) errored = False try: data = iq['ibb_data']['data'] except XMPPError: errored = True self.assertTrue(errored, "ABC=DEFGH did not raise base64 error") def testInvalidBase64PrefixEqual(self): """ Test detecting invalid base64 data with = as a prefix to the character data. """ iq = Iq(xml=ET.fromstring(""" =ABCDEFGH """)) errored = False try: data = iq['ibb_data']['data'] except XMPPError: errored = True self.assertTrue(errored, "=ABCDEFGH did not raise base64 error") def testInvalidBase64Alphabet(self): """ Test detecting invalid base64 data with characters outside of the base64 alphabet. """ iq = Iq(xml=ET.fromstring(""" ABCD?EFGH """)) errored = False try: data = iq['ibb_data']['data'] except XMPPError: errored = True self.assertTrue(errored, "ABCD?EFGH did not raise base64 error") def testConvertData(self): """Test that data is converted to base64""" iq = Iq() iq['type'] = 'set' iq['ibb_data']['seq'] = 0 iq['ibb_data']['data'] = 'slixmpp' self.check(iq, """ c2xpeG1wcA== """) suite = unittest.TestLoader().loadTestsFromTestCase(TestIBB) slixmpp-1.2.2/tests/test_stanza_xep_0004.py0000644000175000001440000002002512603534742021573 0ustar mathieuiusers00000000000000import unittest from slixmpp import Message from slixmpp.test import SlixTest from collections import OrderedDict import slixmpp.plugins.xep_0004 as xep_0004 from slixmpp.xmlstream import register_stanza_plugin class TestDataForms(SlixTest): def setUp(self): register_stanza_plugin(Message, xep_0004.Form) register_stanza_plugin(xep_0004.Form, xep_0004.FormField, iterable=True) register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption, iterable=True) def testMultipleInstructions(self): """Testing using multiple instructions elements in a data form.""" msg = self.Message() msg['form']['instructions'] = "Instructions\nSecond batch" self.check(msg, """ Instructions Second batch """) def testAddField(self): """Testing adding fields to a data form.""" msg = self.Message() form = msg['form'] form.addField(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') self.check(msg, """ A text field Some text! """) fields = OrderedDict() fields['f1'] = {'type': 'text-single', 'label': 'Username', 'required': True} fields['f2'] = {'type': 'text-private', 'label': 'Password', 'required': True} fields['f3'] = {'type': 'text-multi', 'label': 'Message', 'value': 'Enter message.\nA long one even.'} fields['f4'] = {'type': 'list-single', 'label': 'Message Type', 'options': [{'label': 'Cool!', 'value': 'cool'}, {'label': 'Urgh!', 'value': 'urgh'}]} form.set_fields(fields) self.check(msg, """ Enter message. A long one even. """) def testSetValues(self): """Testing setting form values""" msg = self.Message() form = msg['form'] form.add_field(var='foo', ftype='text-single') form.add_field(var='bar', ftype='list-multi') form.setValues({'foo': 'Foo!', 'bar': ['a', 'b']}) self.check(msg, """ Foo! a b """) def testSubmitType(self): """Test that setting type to 'submit' clears extra details""" msg = self.Message() form = msg['form'] fields = OrderedDict() fields['f1'] = {'type': 'text-single', 'label': 'Username', 'required': True} fields['f2'] = {'type': 'text-private', 'label': 'Password', 'required': True} fields['f3'] = {'type': 'text-multi', 'label': 'Message', 'value': 'Enter message.\nA long one even.'} fields['f4'] = {'type': 'list-single', 'label': 'Message Type', 'options': [{'label': 'Cool!', 'value': 'cool'}, {'label': 'Urgh!', 'value': 'urgh'}]} form.set_fields(fields) form['type'] = 'submit' form.set_values({'f1': 'username', 'f2': 'hunter2', 'f3': 'A long\nmultiline\nmessage', 'f4': 'cool'}) self.check(form, """ username hunter2 A long multiline message cool """, use_values=False) def testCancelType(self): """Test that setting type to 'cancel' clears all fields""" msg = self.Message() form = msg['form'] fields = OrderedDict() fields['f1'] = {'type': 'text-single', 'label': 'Username', 'required': True} fields['f2'] = {'type': 'text-private', 'label': 'Password', 'required': True} fields['f3'] = {'type': 'text-multi', 'label': 'Message', 'value': 'Enter message.\nA long one even.'} fields['f4'] = {'type': 'list-single', 'label': 'Message Type', 'options': [{'label': 'Cool!', 'value': 'cool'}, {'label': 'Urgh!', 'value': 'urgh'}]} form.set_fields(fields) form['type'] = 'cancel' self.check(form, """ """) def testReported(self): msg = self.Message() form = msg['form'] form['type'] = 'result' form.add_reported(var='f1', ftype='text-single', label='Username') form.add_item({'f1': 'username@example.org'}) self.check(msg, """ username@example.org """) def testSetReported(self): msg = self.Message() form = msg['form'] form['type'] = 'result' reported = {'f1': { 'var': 'f1', 'type': 'text-single', 'label': 'Username' }} form.set_reported(reported) self.check(msg, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) slixmpp-1.2.2/tests/test_jid.py0000644000175000001440000002554412572154127017535 0ustar mathieuiusers00000000000000# -*- encoding: utf8 -*- from __future__ import unicode_literals import unittest from slixmpp.test import SlixTest from slixmpp import JID, InvalidJID from slixmpp.jid import nodeprep class TestJIDClass(SlixTest): """Verify that the JID class can parse and manipulate JIDs.""" def testJIDFromFull(self): """Test using JID of the form 'user@server/resource/with/slashes'.""" self.check_jid(JID('user@someserver/some/resource'), 'user', 'someserver', 'some/resource', 'user@someserver', 'user@someserver/some/resource', 'user@someserver/some/resource') def testJIDchange(self): """Test changing JID of the form 'user@server/resource/with/slashes'""" j = JID('user1@someserver1/some1/resource1') j.user = 'user' j.domain = 'someserver' j.resource = 'some/resource' self.check_jid(j, 'user', 'someserver', 'some/resource', 'user@someserver', 'user@someserver/some/resource', 'user@someserver/some/resource') def testJIDaliases(self): """Test changing JID using aliases for domain.""" j = JID('user@someserver/resource') j.server = 'anotherserver' self.check_jid(j, domain='anotherserver') j.host = 'yetanother' self.check_jid(j, domain='yetanother') def testJIDSetFullWithUser(self): """Test setting the full JID with a user portion.""" j = JID('user@domain/resource') j.full = 'otheruser@otherdomain/otherresource' self.check_jid(j, 'otheruser', 'otherdomain', 'otherresource', 'otheruser@otherdomain', 'otheruser@otherdomain/otherresource', 'otheruser@otherdomain/otherresource') def testJIDFullNoUserWithResource(self): """ Test setting the full JID without a user portion and with a resource. """ j = JID('user@domain/resource') j.full = 'otherdomain/otherresource' self.check_jid(j, '', 'otherdomain', 'otherresource', 'otherdomain', 'otherdomain/otherresource', 'otherdomain/otherresource') def testJIDFullNoUserNoResource(self): """ Test setting the full JID without a user portion and without a resource. """ j = JID('user@domain/resource') j.full = 'otherdomain' self.check_jid(j, '', 'otherdomain', '', 'otherdomain', 'otherdomain', 'otherdomain') def testJIDBareUser(self): """Test setting the bare JID with a user.""" j = JID('user@domain/resource') j.bare = 'otheruser@otherdomain' self.check_jid(j, 'otheruser', 'otherdomain', 'resource', 'otheruser@otherdomain', 'otheruser@otherdomain/resource', 'otheruser@otherdomain/resource') def testJIDBareNoUser(self): """Test setting the bare JID without a user.""" j = JID('user@domain/resource') j.bare = 'otherdomain' self.check_jid(j, '', 'otherdomain', 'resource', 'otherdomain', 'otherdomain/resource', 'otherdomain/resource') def testJIDNoResource(self): """Test using JID of the form 'user@domain'.""" self.check_jid(JID('user@someserver'), 'user', 'someserver', '', 'user@someserver', 'user@someserver', 'user@someserver') def testJIDNoUser(self): """Test JID of the form 'component.domain.tld'.""" self.check_jid(JID('component.someserver'), '', 'component.someserver', '', 'component.someserver', '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, "Different JIDs are considered equal") self.assertTrue(jid1 != jid2, "Different JIDs are considered equal") def testZeroLengthDomain(self): jid1 = JID('') jid2 = JID() self.assertTrue(jid1 == jid2, "Empty JIDs are not considered equal") self.assertTrue(jid1.domain == '', "Empty JID’s domain part not empty") self.assertTrue(jid1.full == '', "Empty JID’s full part not empty") self.assertRaises(InvalidJID, JID, 'user@') self.assertRaises(InvalidJID, JID, '/resource') self.assertRaises(InvalidJID, JID, 'user@/resource') def testZeroLengthLocalPart(self): self.assertRaises(InvalidJID, JID, '@test.com') self.assertRaises(InvalidJID, JID, '@test.com/resource') def testZeroLengthNodeDomain(self): self.assertRaises(InvalidJID, JID, '@/test.com') def testZeroLengthResource(self): self.assertRaises(InvalidJID, JID, 'test.com/') self.assertRaises(InvalidJID, JID, 'user@test.com/') def test1023LengthDomain(self): domain = ('a.' * 509) + 'a.com' jid = JID('user@%s/resource' % domain) def test1023LengthLocalPart(self): local = 'a' * 1023 jid = JID('%s@test.com' % local) def test1023LengthResource(self): resource = 'r' * 1023 jid = JID('test.com/%s' % resource) def test1024LengthDomain(self): domain = ('a.' * 509) + 'aa.com' self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) self.assertRaises(InvalidJID, JID, 'user@%s' % domain) self.assertRaises(InvalidJID, JID, '%s/resource' % domain) self.assertRaises(InvalidJID, JID, domain) def test1024LengthLocalPart(self): local = 'a' * 1024 self.assertRaises(InvalidJID, JID, '%s@test.com' % local) self.assertRaises(InvalidJID, JID, '%s@test.com/resource' % local) def test1024LengthResource(self): resource = 'r' * 1024 self.assertRaises(InvalidJID, JID, 'test.com/%s' % resource) self.assertRaises(InvalidJID, JID, 'user@test.com/%s' % resource) def testTooLongDomainLabel(self): domain = ('a' * 64) + '.com' self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainEmptyLabel(self): domain = 'aaa..bbb.com' self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainIPv4(self): domain = '127.0.0.1' jid1 = JID('%s' % domain) jid2 = JID('user@%s' % domain) jid3 = JID('%s/resource' % domain) jid4 = JID('user@%s/resource' % domain) def testDomainIPv6(self): domain = '[::1]' jid1 = JID('%s' % domain) jid2 = JID('user@%s' % domain) jid3 = JID('%s/resource' % domain) jid4 = JID('user@%s/resource' % domain) def testDomainInvalidIPv6NoBrackets(self): domain = '::1' self.assertRaises(InvalidJID, JID, '%s' % domain) self.assertRaises(InvalidJID, JID, 'user@%s' % domain) self.assertRaises(InvalidJID, JID, '%s/resource' % domain) self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainInvalidIPv6MissingBracket(self): domain = '[::1' self.assertRaises(InvalidJID, JID, '%s' % domain) self.assertRaises(InvalidJID, JID, 'user@%s' % domain) self.assertRaises(InvalidJID, JID, '%s/resource' % domain) self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainInvalidIPv6WrongBracket(self): domain = '[::]1]' self.assertRaises(InvalidJID, JID, '%s' % domain) self.assertRaises(InvalidJID, JID, 'user@%s' % domain) self.assertRaises(InvalidJID, JID, '%s/resource' % domain) self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainWithPort(self): domain = 'example.com:5555' self.assertRaises(InvalidJID, JID, '%s' % domain) self.assertRaises(InvalidJID, JID, 'user@%s' % domain) self.assertRaises(InvalidJID, JID, '%s/resource' % domain) self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testDomainWithTrailingDot(self): domain = 'example.com.' jid = JID('user@%s/resource' % domain) self.assertEqual(jid.domain, 'example.com') def testDomainWithDashes(self): domain = 'example.com-' self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) domain = '-example.com' self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain) def testACEDomain(self): domain = 'xn--bcher-kva.ch' jid = JID('user@%s/resource' % domain) self.assertEqual(jid.domain.encode('utf-8'), b'b\xc3\xbccher.ch') def testJIDUnescape(self): jid = JID('here\\27s_a_wild_\\26_\\2fcr%zy\\2f_\\40ddress\\20for\\3a\\3cwv\\3e(\\22IMPS\\22)\\5c@example.com') ujid = jid.unescape() self.assertEqual(ujid.local, 'here\'s_a_wild_&_/cr%zy/_@ddress for:("imps")\\') jid = JID('blah\\5cfoo\\5c20bar@example.com') ujid = jid.unescape() self.assertEqual(ujid.local, 'blah\\foo\\20bar') def testStartOrEndWithEscapedSpaces(self): local = ' foo' self.assertRaises(InvalidJID, JID, '%s@example.com' % local) local = 'bar ' self.assertRaises(InvalidJID, JID, '%s@example.com' % local) # Need more input for these cases. A JID starting with \20 *is* valid # according to RFC 6122, but is not according to XEP-0106. #self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2') #self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20') def testNodePrepIdemptotent(self): node = 'ᴹᴵᴷᴬᴱᴸ' self.assertEqual(nodeprep(node), nodeprep(nodeprep(node))) suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass) slixmpp-1.2.2/tests/test_stream_xep_0325.py0000644000175000001440000002754612603534742021613 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ import sys import datetime import time import threading from slixmpp.test import * from slixmpp.xmlstream import ElementBase from slixmpp.plugins.xep_0325.device import Device class TestStreamControl(SlixTest): """ Test using the XEP-0325 plugin. """ def setUp(self): pass def _time_now(self): return datetime.datetime.now().replace(microsecond=0).isoformat() def tearDown(self): self.stream_close() def testRequestSetOk(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) myDevice = Device("Device22") myDevice._add_control_field(name="Temperature", typename="int", value="15") self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) self.recv(""" """) self.send(""" """) self.assertEqual(myDevice._get_field_value("Temperature"), "17") def testRequestSetMulti(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) myDevice = Device("Device22") myDevice._add_control_field(name="Temperature", typename="int", value="15") myDevice._add_control_field(name="Startup", typename="date", value="2013-01-03") myDevice2 = Device("Device23") myDevice2._add_control_field(name="Temperature", typename="int", value="19") myDevice2._add_control_field(name="Startup", typename="date", value="2013-01-09") self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) self.xmpp['xep_0325'].register_node(nodeId="Device23", device=myDevice2, commTimeout=0.5) self.recv(""" """) self.send(""" """) self.assertEqual(myDevice._get_field_value("Temperature"), "17") self.assertEqual(myDevice2._get_field_value("Temperature"), "19") self.recv(""" """) self.send(""" """) self.assertEqual(myDevice._get_field_value("Temperature"), "20") self.assertEqual(myDevice2._get_field_value("Temperature"), "20") self.assertEqual(myDevice._get_field_value("Startup"), "2013-02-01") self.assertEqual(myDevice2._get_field_value("Startup"), "2013-02-01") def testRequestSetFail(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) myDevice = Device("Device23") myDevice._add_control_field(name="Temperature", typename="int", value="15") self.xmpp['xep_0325'].register_node(nodeId="Device23", device=myDevice, commTimeout=0.5) self.recv(""" """) self.send(""" Invalid field Voltage """) self.assertEqual(myDevice._get_field_value("Temperature"), "15") self.assertFalse(myDevice.has_control_field("Voltage", "int")) def testDirectSetOk(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) myDevice = Device("Device22") myDevice._add_control_field(name="Temperature", typename="int", value="15") self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) self.recv(""" """) time.sleep(0.5) self.assertEqual(myDevice._get_field_value("Temperature"), "17") def testDirectSetFail(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) myDevice = Device("Device22") myDevice._add_control_field(name="Temperature", typename="int", value="15") self.xmpp['xep_0325'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) self.recv(""" """) self.assertEqual(myDevice._get_field_value("Temperature"), "15"); self.assertFalse(myDevice.has_control_field("Voltage", "int")); def testRequestSetOkAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0325']) results = [] def my_callback(from_jid, result, nodeIds=None, fields=None, error_msg=None): results.append(result) fields = [] fields.append(("Temperature", "double", "20.5")) fields.append(("TemperatureAlarmSetting", "string", "High")) self.xmpp['xep_0325'].set_request(from_jid="tester@localhost", to_jid="you@google.com", fields=fields, nodeIds=['Device33', 'Device22'], callback=my_callback) self.send(""" """) self.recv(""" """) self.assertEqual(results, ["OK"]); def testRequestSetErrorAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0325']) results = [] def my_callback(from_jid, result, nodeIds=None, fields=None, error_msg=None): results.append(result) fields = [] fields.append(("Temperature", "double", "20.5")) fields.append(("TemperatureAlarmSetting", "string", "High")) self.xmpp['xep_0325'].set_request(from_jid="tester@localhost", to_jid="you@google.com", fields=fields, nodeIds=['Device33', 'Device22'], callback=my_callback) self.send(""" """) self.recv(""" Sensor error """) self.assertEqual(results, ["OtherError"]) def testServiceDiscoveryClient(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0325']) self.recv(""" """) self.send(""" """) def testServiceDiscoveryComponent(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0325']) self.recv(""" """) self.send(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamControl) slixmpp-1.2.2/tests/test_overall.py0000644000175000001440000000121212424504520020405 0ustar mathieuiusers00000000000000import os import re import sys import unittest import tabnanny import compileall class TestOverall(unittest.TestCase): """ Test overall package health by compiling and checking code style. """ def testModules(self): """Testing all modules by compiling them""" src = '.%sslixmpp' % os.sep rx = re.compile('/[.]svn|.*26.*') self.failUnless(compileall.compile_dir(src, rx=rx, quiet=True)) def testTabNanny(self): """Testing that indentation is consistent""" self.failIf(tabnanny.check('..%sslixmpp' % os.sep)) suite = unittest.TestLoader().loadTestsFromTestCase(TestOverall) slixmpp-1.2.2/tests/__init__.py0000644000175000001440000000000012466746717017461 0ustar mathieuiusers00000000000000slixmpp-1.2.2/tests/test_stanza_xep_0059.py0000644000175000001440000000550212466746741021623 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.plugins.xep_0059 import Set from slixmpp.xmlstream import ET class TestSetStanzas(SlixTest): def testSetFirstIndex(self): s = Set() s['first'] = 'id' s.set_first_index('10') self.check(s, """ id """) def testGetFirstIndex(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) expected = '10' self.failUnless(s['first_index'] == expected) def testDelFirstIndex(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) del s['first_index'] self.check(s, """ id """) def testSetBefore(self): s = Set() s['before'] = True self.check(s, """ """) def testGetBefore(self): xml_string = """ """ s = Set(ET.fromstring(xml_string)) expected = True self.failUnless(s['before'] == expected) def testGetBefore(self): xml_string = """ """ s = Set(ET.fromstring(xml_string)) del s['before'] self.check(s, """ """) def testSetBeforeVal(self): s = Set() s['before'] = 'id' self.check(s, """ id """) def testGetBeforeVal(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) expected = 'id' self.failUnless(s['before'] == expected) def testGetBeforeVal(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) del s['before'] self.check(s, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestSetStanzas) slixmpp-1.2.2/tests/test_stream.py0000644000175000001440000000322212467067322020252 0ustar mathieuiusers00000000000000import time import unittest from slixmpp.test import SlixTest class TestStreamTester(SlixTest): """ Test that we can simulate and test a stanza stream. """ def tearDown(self): self.stream_close() def testClientEcho(self): """Test that we can interact with a ClientXMPP instance.""" self.stream_start(mode='client') def echo(msg): msg.reply('Thanks for sending: %s' % msg['body']).send() self.xmpp.add_event_handler('message', echo) self.recv(""" Hi! """) self.send(""" Thanks for sending: Hi! """) def testComponentEcho(self): """Test that we can interact with a ComponentXMPP instance.""" self.stream_start(mode='component') def echo(msg): msg.reply('Thanks for sending: %(body)s' % msg).send() self.xmpp.add_event_handler('message', echo) self.recv(""" Hi! """) self.send(""" Thanks for sending: Hi! """) def testSendStreamHeader(self): """Test that we can check a sent stream header.""" self.stream_start(mode='client', skip=False) self.send_header(sto='localhost') suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester) slixmpp-1.2.2/tests/test_stanza_xep_0122.py0000644000175000001440000001460712603534742021605 0ustar mathieuiusers00000000000000import unittest from slixmpp import Message from slixmpp.test import SlixTest import slixmpp.plugins.xep_0004 as xep_0004 import slixmpp.plugins.xep_0122 as xep_0122 from slixmpp.xmlstream import register_stanza_plugin class TestDataForms(SlixTest): def setUp(self): register_stanza_plugin(Message, xep_0004.Form) register_stanza_plugin(xep_0004.Form, xep_0004.FormField, iterable=True) register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption, iterable=True) register_stanza_plugin(xep_0004.FormField, xep_0122.FormValidation) def test_basic_validation(self): """Testing basic validation setting and getting.""" msg = self.Message() form = msg['form'] field = form.add_field(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') validation = field['validate'] validation['datatype'] = 'xs:string' validation.set_basic(True) self.check(msg, """ A text field Some text! """) self.assertTrue(validation.get_basic()) self.assertFalse(validation.get_open()) self.assertFalse(validation.get_range()) self.assertFalse(validation.get_regex()) def test_open_validation(self): """Testing open validation setting and getting.""" msg = self.Message() form = msg['form'] field = form.add_field(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') validation = field['validate'] validation.set_open(True) self.check(msg, """ A text field Some text! """) self.assertFalse(validation.get_basic()) self.assertTrue(validation.get_open()) self.assertFalse(validation.get_range()) self.assertFalse(validation.get_regex()) def test_regex_validation(self): """Testing regex validation setting and getting.""" msg = self.Message() form = msg['form'] field = form.add_field(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') regex_value = '[0-9]+' validation = field['validate'] validation.set_regex(regex_value) self.check(msg, """ A text field Some text! [0-9]+ """) self.assertFalse(validation.get_basic()) self.assertFalse(validation.get_open()) self.assertFalse(validation.get_range()) self.assertTrue(validation.get_regex()) self.assertEqual(regex_value, validation.get_regex()) def test_range_validation(self): """Testing range validation setting and getting.""" msg = self.Message() form = msg['form'] field = form.add_field(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') validation = field['validate'] validation.set_range(True, minimum=0, maximum=10) self.check(msg, """ A text field Some text! """) self.assertDictEqual(dict(minimum=str(0), maximum=str(10)), validation.get_range()) def test_reported_field_validation(self): """ Testing adding validation to the field when it's stored in the reported. :return: """ msg = self.Message() form = msg['form'] field = form.add_reported(var='f1', ftype='text-single', label='Text') validation = field['validate'] validation.set_basic(True) form.add_item({'f1': 'Some text!'}) self.check(msg, """ Some text! """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) slixmpp-1.2.2/tests/test_stanza_xep_0009.py0000644000175000001440000002656612466746741021633 0ustar mathieuiusers00000000000000# -*- encoding:utf-8 -*- """ Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import unicode_literals import base64 import sys from slixmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \ MethodResponse from slixmpp.plugins.xep_0009.binding import py2xml, xml2py, rpcbase64, \ rpctime from slixmpp.stanza.iq import Iq from slixmpp.test.slixtest import SlixTest from slixmpp.xmlstream.stanzabase import register_stanza_plugin from slixmpp.xmlstream.tostring import tostring import unittest class TestJabberRPC(SlixTest): def setUp(self): register_stanza_plugin(Iq, RPCQuery) register_stanza_plugin(RPCQuery, MethodCall) register_stanza_plugin(RPCQuery, MethodResponse) def testMethodCall(self): iq = self.Iq() iq['rpc_query']['method_call']['method_name'] = 'system.exit' iq['rpc_query']['method_call']['params'] = py2xml(*()) self.check(iq, """ system.exit """, use_values=False) def testMethodResponse(self): iq = self.Iq() iq['rpc_query']['method_response']['params'] = py2xml(*()) self.check(iq, """ """, use_values=False) def testConvertNil(self): params = [None] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" """) self.assertTrue(self.compare(expected_xml, params_xml), "Nil to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to nil conversion") def testConvertBoolean(self): params = [True, False] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 1 0 """) self.assertTrue(self.compare(expected_xml, params_xml), "Boolean to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to boolean conversion") def testConvertString(self): params = ["'This' & \"That\""] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 'This' & "That" """) self.assertTrue(self.compare(expected_xml, params_xml), "String to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to string conversion") def testConvertUnicodeString(self): params = ["おはよう"] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" おはよう """) self.assertTrue(self.compare(expected_xml, params_xml), "String to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to string conversion") def testConvertInteger(self): params = [32767, -32768] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 32767 -32768 """) alternate_xml = self.parse_xml(""" 32767 -32768 """) self.assertTrue(self.compare(expected_xml, params_xml), "Integer to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to boolean conversion") self.assertEqual(params, xml2py(alternate_xml), "Alternate XML to boolean conversion") def testConvertDouble(self): params = [3.14159265] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 3.14159265 """) self.assertTrue(self.compare(expected_xml, params_xml), "Double to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to double conversion") def testConvertBase64(self): params = [rpcbase64(base64.b64encode(b"Hello, world!"))] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" SGVsbG8sIHdvcmxkIQ== """) alternate_xml = self.parse_xml(""" SGVsbG8sIHdvcmxkIQ== """) self.assertTrue(self.compare(expected_xml, params_xml), "Base64 to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(list(map(lambda x: x.decode(), params)), list(map(lambda x: x.decode(), xml2py(expected_xml))), "XML to base64 conversion") self.assertEqual(list(map(lambda x: x.decode(), params)), list(map(lambda x: x.decode(), xml2py(alternate_xml))), "Alternate XML to base64 conversion") def testConvertDateTime(self): params = [rpctime("20111220T01:50:00")] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 20111220T01:50:00 """) self.assertTrue(self.compare(expected_xml, params_xml), "DateTime to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(list(map(lambda x: x.iso8601(), params)), list(map(lambda x: x.iso8601(), xml2py(expected_xml))), None) def testConvertArray(self): params = [[1,2,3], ('a', 'b', 'c')] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" 1 2 3 a b c """) self.assertTrue(self.compare(expected_xml, params_xml), "Array to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(list(map(list, params)), xml2py(expected_xml), "XML to array conversion") def testConvertStruct(self): params = [{"foo": "bar", "baz": False}] params_xml = py2xml(*params) expected_xml = self.parse_xml(""" foo bar baz 0 """) self.assertTrue(self.compare(expected_xml, params_xml), "Struct to XML conversion\nExpected: %s\nGot: %s" % ( tostring(expected_xml), tostring(params_xml))) self.assertEqual(params, xml2py(expected_xml), "XML to struct conversion") suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) slixmpp-1.2.2/tests/test_stream_exceptions.py0000644000175000001440000001434312467062123022513 0ustar mathieuiusers00000000000000from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.exceptions import XMPPError import unittest from slixmpp.test import SlixTest class TestStreamExceptions(SlixTest): """ Test handling roster updates. """ def tearDown(self): self.stream_close() def testExceptionContinueWorking(self): """Test that Slixmpp continues to respond after an XMPPError is raised.""" def message(msg): raise XMPPError(clear=True) self.stream_start() self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" """) self.recv(""" This is going to cause an error. """) self.send(""" """) def testXMPPErrorException(self): """Test raising an XMPPError exception.""" def message(msg): raise XMPPError(condition='feature-not-implemented', text="We don't do things that way here.", etype='cancel', extension='foo', extension_ns='foo:error', extension_args={'test': 'true'}) self.stream_start() self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" We don't do things that way here. """, use_values=False) def testIqErrorException(self): """Test using error exceptions with Iq stanzas.""" def handle_iq(iq): raise XMPPError(condition='feature-not-implemented', text="We don't do things that way here.", etype='cancel', clear=False) self.stream_start() self.xmpp.register_handler( Callback( 'Test Iq', MatchXPath('{%s}iq/{test}query' % self.xmpp.default_ns), handle_iq)) self.recv(""" """) self.send(""" We don't do things that way here. """, use_values=False) def testUnknownException(self): """Test raising an generic exception in a handler.""" raised_errors = [] def message(msg): raise ValueError("Did something wrong") def catch_error(*args, **kwargs): raised_errors.append(True) self.stream_start() self.xmpp.exception = catch_error self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" Slixmpp got into trouble. """) self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors) def testUnknownException(self): """Test Slixmpp continues to respond after an unknown exception.""" raised_errors = [] def message(msg): raise ValueError("Did something wrong") def catch_error(*args, **kwargs): raised_errors.append(True) self.stream_start() self.xmpp.exception = catch_error self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" Slixmpp got into trouble. """) self.recv(""" This is going to cause an error. """) self.send(""" Slixmpp got into trouble. """) self.assertEqual(raised_errors, [True, True], "Exceptions were not raised: %s" % raised_errors) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions) slixmpp-1.2.2/tests/test_stanza_gmail.py0000644000175000001440000001031212466746741021436 0ustar mathieuiusers00000000000000import unittest from slixmpp import Iq from slixmpp.test import SlixTest import slixmpp.plugins.gmail_notify as gmail from slixmpp.xmlstream import register_stanza_plugin, ET class TestGmail(SlixTest): def setUp(self): register_stanza_plugin(Iq, gmail.GmailQuery) register_stanza_plugin(Iq, gmail.MailBox) register_stanza_plugin(Iq, gmail.NewMail) def testCreateQuery(self): """Testing querying Gmail for emails.""" iq = self.Iq() iq['type'] = 'get' iq['gmail']['search'] = 'is:starred' iq['gmail']['newer-than-time'] = '1140638252542' iq['gmail']['newer-than-tid'] = '11134623426430234' self.check(iq, """ """, use_values=False) def testMailBox(self): """Testing reading from Gmail mailbox result""" # Use the example from Google's documentation at # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications xml = ET.fromstring(""" act1scene3 Put thy rapier up. Ay, ay, a scratch, a scratch; marry, 'tis enough. """) iq = self.Iq(xml=xml) mailbox = iq['mailbox'] self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match") self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match") self.failUnless(mailbox['matched'] == '95', "total-matched incorrect") self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect") self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads") thread = mailbox['threads'][0] self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match") self.failUnless(thread['participation'] == '1', "thread participation incorrect") self.failUnless(thread['messages'] == '28', "thread message count incorrect") self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match") self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match") self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect") self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match") self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match") self.failUnless(len(thread['senders']) == 3, "could not extract senders") sender1 = thread['senders'][0] self.failUnless(sender1['name'] == 'Me', "sender name doesn't match") self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match") self.failUnless(sender1['originator'] == True, "sender originator incorrect") self.failUnless(sender1['unread'] == False, "sender unread incorrectly True") sender2 = thread['senders'][2] self.failUnless(sender2['unread'] == True, "sender unread incorrectly False") suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail) slixmpp-1.2.2/tests/test_stanza_presence.py0000644000175000001440000000431012466746741022152 0ustar mathieuiusers00000000000000import unittest import slixmpp from slixmpp.test import SlixTest class TestPresenceStanzas(SlixTest): def testPresenceShowRegression(self): """Regression check presence['type'] = 'dnd' show value working""" p = self.Presence() p['type'] = 'dnd' self.check(p, "dnd") def testPresenceType(self): """Test manipulating presence['type']""" p = self.Presence() p['type'] = 'available' self.check(p, "") self.failUnless(p['type'] == 'available', "Incorrect presence['type'] for type 'available': %s" % p['type']) for showtype in ['away', 'chat', 'dnd', 'xa']: p['type'] = showtype self.check(p, """ %s """ % showtype) self.failUnless(p['type'] == showtype, "Incorrect presence['type'] for type '%s'" % showtype) p['type'] = None self.check(p, "") def testPresenceUnsolicitedOffline(self): """ Unsolicted offline presence does not spawn changed_status or update the roster. """ p = self.Presence() p['type'] = 'unavailable' p['from'] = 'bill@chadmore.com/gmail15af' c = slixmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handlechangedpresence(event): happened.append(True) c.add_event_handler("changed_status", handlechangedpresence) c._handle_presence(p) self.failUnless(happened == [], "changed_status event triggered for extra unavailable presence") roster = c.roster['crap@wherever'] self.failUnless(roster['bill@chadmore.com'].resources == {}, "Roster updated for superfulous unavailable presence") def testNickPlugin(self): """Test presence/nick/nick stanza.""" p = self.Presence() p['nick']['nick'] = 'A nickname!' self.check(p, """ A nickname! """) suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) slixmpp-1.2.2/tests/test_stream_handlers.py0000644000175000001440000001446712603534733022144 0ustar mathieuiusers00000000000000import time import threading import unittest from slixmpp.test import SlixTest from slixmpp.exceptions import IqTimeout from slixmpp import Callback, MatchXPath class TestHandlers(SlixTest): """ Test using handlers and waiters. """ def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testCallback(self): """Test using stream callback handlers.""" def callback_handler(stanza): self.xmpp.send_raw(""" Success! """) callback = Callback('Test Callback', MatchXPath('{test}tester'), callback_handler) self.xmpp.register_handler(callback) self.recv("""""") msg = self.Message() msg['body'] = 'Success!' self.send(msg) def testWaiter(self): """Test using stream waiter handler.""" def waiter_handler(stanza): iq = self.xmpp.Iq() iq['id'] = 'test' iq['type'] = 'set' iq['query'] = 'test' def callback_waiter(result): self.xmpp.send_raw(""" Successful: %s """ % result['query']) iq.send(callback=callback_waiter) self.xmpp.add_event_handler('message', waiter_handler) # Send message to trigger waiter_handler self.recv(""" Testing """) # Check that Iq was sent by waiter_handler iq = self.Iq() iq['id'] = 'test' iq['type'] = 'set' iq['query'] = 'test' self.send(iq) # Send the reply Iq self.recv(""" """) # Check that waiter_handler received the reply msg = self.Message() msg['body'] = 'Successful: test' self.send(msg) def testWaiterTimeout(self): """Test that waiter handler is removed after timeout.""" def waiter_handler(stanza): iq = self.xmpp.Iq() iq['id'] = 'test2' iq['type'] = 'set' iq['query'] = 'test2' try: reply = iq.send(timeout=0) except IqTimeout: pass self.xmpp.add_event_handler('message', waiter_handler) # Start test by triggerig waiter_handler self.recv("""Start Test""") # Check that Iq was sent to trigger start of timeout period iq = self.Iq() iq['id'] = 'test2' iq['type'] = 'set' iq['query'] = 'test2' self.send(iq) # Check that the waiter is no longer registered waiter_exists = self.xmpp.remove_handler('IqWait_test2') self.failUnless(waiter_exists == False, "Waiter handler was not removed.") def testIqCallback(self): """Test that iq.send(callback=handle_foo) works.""" events = [] def handle_foo(iq): events.append('foo') iq = self.Iq() iq['type'] = 'get' iq['id'] = 'test-foo' iq['to'] = 'user@localhost' iq['query'] = 'foo' iq.send(callback=handle_foo) self.send(""" """) self.recv(""" """) self.failUnless(events == ['foo'], "Iq callback was not executed: %s" % events) def testMultipleHandlersForStanza(self): """ Test that multiple handlers for a single stanza work without clobbering each other. """ def handler_1(msg): msg.reply("Handler 1: %s" % msg['body']).send() def handler_2(msg): msg.reply("Handler 2: %s" % msg['body']).send() def handler_3(msg): msg.reply("Handler 3: %s" % msg['body']).send() self.xmpp.add_event_handler('message', handler_1) self.xmpp.add_event_handler('message', handler_2) self.xmpp.add_event_handler('message', handler_3) self.recv(""" Testing """) # This test is brittle, depending on the fact that handlers # will be checked in the order they are registered. self.send(""" Handler 1: Testing """) self.send(""" Handler 2: Testing """) self.send(""" Handler 3: Testing """) def testWrongSender(self): """ Test that using the wrong sender JID in a IQ result doesn't trigger handlers. """ events = [] def callback(result): events.append(result['from'].full) iq = self.Iq() iq['id'] = 'test' iq['to'] = 'tester@slixmpp.com/test' iq['type'] = 'set' iq['query'] = 'test' iq.send(callback=callback) self.recv(""" """) self.recv(""" """) self.recv(""" """) # Now for a good one self.recv(""" """) self.assertEqual(events, ['tester@slixmpp.com/test'], "Did not timeout on bad sender") suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers) slixmpp-1.2.2/tests/test_stanza_xep_0085.py0000644000175000001440000000304012466746741021615 0ustar mathieuiusers00000000000000import unittest from slixmpp import Message from slixmpp.test import SlixTest import slixmpp.plugins.xep_0085 as xep_0085 from slixmpp.xmlstream import register_stanza_plugin class TestChatStates(SlixTest): def setUp(self): register_stanza_plugin(Message, xep_0085.stanza.Active) register_stanza_plugin(Message, xep_0085.stanza.Composing) register_stanza_plugin(Message, xep_0085.stanza.Gone) register_stanza_plugin(Message, xep_0085.stanza.Inactive) register_stanza_plugin(Message, xep_0085.stanza.Paused) def testCreateChatState(self): """Testing creating chat states.""" xmlstring = """ <%s xmlns="http://jabber.org/protocol/chatstates" /> """ msg = self.Message() self.assertEqual(msg['chat_state'], '') self.check(msg, "", use_values=False) msg['chat_state'] = 'active' self.check(msg, xmlstring % 'active', use_values=False) msg['chat_state'] = 'composing' self.check(msg, xmlstring % 'composing', use_values=False) msg['chat_state'] = 'gone' self.check(msg, xmlstring % 'gone', use_values=False) msg['chat_state'] = 'inactive' self.check(msg, xmlstring % 'inactive', use_values=False) msg['chat_state'] = 'paused' self.check(msg, xmlstring % 'paused', use_values=False) del msg['chat_state'] self.check(msg, "") suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) slixmpp-1.2.2/tests/test_stanza_base.py0000644000175000001440000000562012467060426021253 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream.stanzabase import ET, StanzaBase class TestStanzaBase(SlixTest): def testTo(self): """Test the 'to' interface of StanzaBase.""" stanza = StanzaBase() stanza['to'] = 'user@example.com' self.failUnless(str(stanza['to']) == 'user@example.com', "Setting and retrieving stanza 'to' attribute did not work.") def testFrom(self): """Test the 'from' interface of StanzaBase.""" stanza = StanzaBase() stanza['from'] = 'user@example.com' self.failUnless(str(stanza['from']) == 'user@example.com', "Setting and retrieving stanza 'from' attribute did not work.") def testPayload(self): """Test the 'payload' interface of StanzaBase.""" stanza = StanzaBase() self.failUnless(stanza['payload'] == [], "Empty stanza does not have an empty payload.") stanza['payload'] = ET.Element("{foo}foo") self.failUnless(len(stanza['payload']) == 1, "Stanza contents and payload do not match.") stanza['payload'] = ET.Element('{bar}bar') self.failUnless(len(stanza['payload']) == 2, "Stanza payload was not appended.") del stanza['payload'] self.failUnless(stanza['payload'] == [], "Stanza payload not cleared after deletion.") stanza['payload'] = [ET.Element('{foo}foo'), ET.Element('{bar}bar')] self.failUnless(len(stanza['payload']) == 2, "Adding multiple elements to stanza's payload did not work.") def testClear(self): """Test clearing a stanza.""" stanza = StanzaBase() stanza['to'] = 'user@example.com' stanza['payload'] = ET.Element("{foo}foo") stanza.clear() self.failUnless(stanza['payload'] == [], "Stanza payload was not cleared after calling .clear()") self.failUnless(str(stanza['to']) == "user@example.com", "Stanza attributes were not preserved after calling .clear()") def testReply(self): """Test creating a reply stanza.""" stanza = StanzaBase() stanza['to'] = "recipient@example.com" stanza['from'] = "sender@example.com" stanza['payload'] = ET.Element("{foo}foo") stanza = stanza.reply() self.failUnless(str(stanza['to'] == "sender@example.com"), "Stanza reply did not change 'to' attribute.") self.failUnless(stanza['payload'] == [], "Stanza reply did not empty stanza payload.") def testError(self): """Test marking a stanza as an error.""" stanza = StanzaBase() stanza['type'] = 'get' stanza.error() self.failUnless(stanza['type'] == 'error', "Stanza type is not 'error' after calling error()") suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase) slixmpp-1.2.2/tests/test_stanza_message.py0000644000175000001440000000335312774765074022002 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.stanza.message import Message from slixmpp.stanza.htmlim import HTMLIM from slixmpp.xmlstream import register_stanza_plugin class TestMessageStanzas(SlixTest): def setUp(self): register_stanza_plugin(Message, HTMLIM) def testGroupchatReplyRegression(self): "Regression groupchat reply should be to barejid" msg = self.Message() msg['to'] = 'me@myserver.tld' msg['from'] = 'room@someservice.someserver.tld/somenick' msg['type'] = 'groupchat' msg['body'] = "this is a message" msg = msg.reply() self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') def testHTMLPlugin(self): "Test message/html/body stanza" msg = self.Message() msg['to'] = "fritzy@netflint.net/slixmpp" msg['body'] = "this is the plaintext message" msg['type'] = 'chat' msg['html']['body'] = '

This is the htmlim message

' self.check(msg, """ this is the plaintext message

This is the htmlim message

""") def testNickPlugin(self): "Test message/nick/nick stanza." msg = self.Message() msg['nick']['nick'] = 'A nickname!' self.check(msg, """ A nickname! """) suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) slixmpp-1.2.2/tests/test_stream_xep_0047.py0000644000175000001440000001042413004224717021571 0ustar mathieuiusers00000000000000import asyncio import threading import time import unittest from slixmpp.test import SlixTest class TestInBandByteStreams(SlixTest): def setUp(self): self.stream_start(plugins=['xep_0047', 'xep_0030']) def tearDown(self): self.stream_close() def testOpenStream(self): """Test requesting a stream, successfully""" events = [] def on_stream_start(stream): events.append('ibb_stream_start') self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) self.xmpp['xep_0047'].open_stream('tester@localhost/receiver', sid='testing') self.send(""" """) self.recv(""" """) self.assertEqual(events, ['ibb_stream_start']) def testAysncOpenStream(self): """Test requesting a stream, aysnc""" events = set() def on_stream_start(stream): events.add('ibb_stream_start') def stream_callback(iq): events.add('callback') self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) self.xmpp['xep_0047'].open_stream('tester@localhost/receiver', sid='testing', callback=stream_callback) self.send(""" """) self.recv(""" """) self.assertEqual(events, {'ibb_stream_start', 'callback'}) @asyncio.coroutine def testSendData(self): """Test sending data over an in-band bytestream.""" streams = [] data = [] def on_stream_start(stream): streams.append(stream) def on_stream_data(d): data.append(d['data']) self.xmpp.add_event_handler('ibb_stream_start', on_stream_start) self.xmpp.add_event_handler('ibb_stream_data', on_stream_data) self.xmpp['xep_0047'].open_stream('tester@localhost/receiver', sid='testing') self.send(""" """) self.recv(""" """) stream = streams[0] # Test sending data out yield from stream.send("Testing") self.send(""" VGVzdGluZw== """) self.recv(""" """) # Test receiving data self.recv(""" aXQgd29ya3Mh """) self.send(""" """) self.assertEqual(data, [b'it works!']) suite = unittest.TestLoader().loadTestsFromTestCase(TestInBandByteStreams) slixmpp-1.2.2/tests/test_stanza_xep_0050.py0000644000175000001440000000701613004224717021573 0ustar mathieuiusers00000000000000from slixmpp import Iq import unittest from slixmpp.test import SlixTest from slixmpp.plugins.xep_0050 import Command from slixmpp.xmlstream import register_stanza_plugin class TestAdHocCommandStanzas(SlixTest): def setUp(self): register_stanza_plugin(Iq, Command) def testAction(self): """Test using the action attribute.""" iq = self.Iq() iq['type'] = 'set' iq['command']['node'] = 'foo' iq['command']['action'] = 'execute' self.failUnless(iq['command']['action'] == 'execute') iq['command']['action'] = 'complete' self.failUnless(iq['command']['action'] == 'complete') iq['command']['action'] = 'cancel' self.failUnless(iq['command']['action'] == 'cancel') def testSetActions(self): """Test setting next actions in a command stanza.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] self.check(iq, """ """) def testGetActions(self): """Test retrieving next actions from a command stanza.""" iq = self.Iq() iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] results = iq['command']['actions'] expected = {'prev', 'next'} self.assertEqual(results, expected, "Incorrect next actions: %s" % results) def testDelActions(self): """Test removing next actions from a command stanza.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] del iq['command']['actions'] self.check(iq, """ """) def testAddNote(self): """Test adding a command note.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command'].add_note('Danger!', ntype='warning') self.check(iq, """ Danger! """) def testNotes(self): """Test using command notes.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' notes = [('info', 'Interesting...'), ('warning', 'Danger!'), ('error', "I can't let you do that")] iq['command']['notes'] = notes self.failUnless(iq['command']['notes'] == notes, "Notes don't match: %s %s" % (notes, iq['command']['notes'])) self.check(iq, """ Interesting... Danger! I can't let you do that """) suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommandStanzas) slixmpp-1.2.2/tests/test_stanza_roster.py0000644000175000001440000000570112466746741021671 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream import ET class TestRosterStanzas(SlixTest): def testAddItems(self): """Test adding items to a roster stanza.""" iq = self.Iq() iq['roster'].set_items({ 'user@example.com': { 'name': 'User', 'subscription': 'both', 'groups': ['Friends', 'Coworkers']}, 'otheruser@example.com': { 'name': 'Other User', 'subscription': 'both', 'groups': []}}) self.check(iq, """ Friends Coworkers """) def testGetItems(self): """Test retrieving items from a roster stanza.""" xml_string = """ Friends Coworkers """ iq = self.Iq(ET.fromstring(xml_string)) expected = { 'user@example.com': { 'name': 'User', 'subscription': 'both', 'ask': '', 'approved': '', 'groups': ['Friends', 'Coworkers']}, 'otheruser@example.com': { 'name': 'Other User', 'subscription': 'both', 'ask': '', 'approved': '', 'groups': []}} debug = "Roster items don't match after retrieval." debug += "\nReturned: %s" % str(iq['roster']['items']) debug += "\nExpected: %s" % str(expected) self.failUnless(iq['roster']['items'] == expected, debug) def testDelItems(self): """Test clearing items from a roster stanza.""" xml_string = """ Friends Coworkers """ iq = self.Iq(ET.fromstring(xml_string)) del iq['roster']['items'] self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas) slixmpp-1.2.2/tests/test_stanza_xep_0060.py0000644000175000001440000005735412775000453021611 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest import slixmpp.plugins.xep_0004 as xep_0004 import slixmpp.plugins.xep_0060.stanza as pubsub from slixmpp.xmlstream.stanzabase import ET class TestPubsubStanzas(SlixTest): def testAffiliations(self): "Testing iq/pubsub/affiliations/affiliation stanzas" iq = self.Iq() aff1 = pubsub.Affiliation() aff1['node'] = 'testnode' aff1['affiliation'] = 'owner' aff2 = pubsub.Affiliation() aff2['node'] = 'testnode2' aff2['affiliation'] = 'publisher' iq['pubsub']['affiliations'].append(aff1) iq['pubsub']['affiliations'].append(aff2) self.check(iq, """ """) def testSubscriptions(self): "Testing iq/pubsub/subscriptions/subscription stanzas" iq = self.Iq() sub1 = pubsub.Subscription() sub1['node'] = 'testnode' sub1['jid'] = 'steve@myserver.tld/someresource' sub2 = pubsub.Subscription() sub2['node'] = 'testnode2' sub2['jid'] = 'boogers@bork.top/bill' sub2['subscription'] = 'subscribed' iq['pubsub']['subscriptions'].append(sub1) iq['pubsub']['subscriptions'].append(sub2) self.check(iq, """ """) def testOptionalSettings(self): "Testing iq/pubsub/subscription/subscribe-options stanzas" iq = self.Iq() iq['pubsub']['subscription']['suboptions']['required'] = True iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/slixmpp" iq['pubsub']['subscription']['subscription'] = 'unconfigured' self.check(iq, """ """) def testItems(self): "Testing iq/pubsub/items stanzas" iq = self.Iq() iq['pubsub']['items']['node'] = 'crap' payload = ET.fromstring(""" """) payload2 = ET.fromstring(""" """) item = pubsub.Item() item['id'] = 'asdf' item['payload'] = payload item2 = pubsub.Item() item2['id'] = 'asdf2' item2['payload'] = payload2 iq['pubsub']['items'].append(item) iq['pubsub']['items'].append(item2) self.check(iq, """ """) def testCreate(self): "Testing iq/pubsub/create&configure stanzas" iq = self.Iq() iq['pubsub']['create']['node'] = 'mynode' iq['pubsub']['configure']['form'].addField('pubsub#title', ftype='text-single', value='This thing is awesome') self.check(iq, """ This thing is awesome """) def testDefault(self): "Testing iq/pubsub_owner/default stanzas" iq = self.Iq() iq['pubsub_owner']['default'] iq['pubsub_owner']['default']['node'] = 'mynode' iq['pubsub_owner']['default']['form'].add_field('pubsub#title', ftype='text-single', value='This thing is awesome') self.check(iq, """ This thing is awesome """, use_values=False) def testSubscribe(self): "testing iq/pubsub/subscribe stanzas" iq = self.Iq() iq['pubsub']['subscribe']['options'] iq['pubsub']['subscribe']['node'] = 'cheese' iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/slixmpp' iq['pubsub']['subscribe']['options']['node'] = 'cheese' iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/slixmpp' form = xep_0004.Form() form['type'] = 'submit' form.add_field('pubsub#title', ftype='text-single', value='this thing is awesome') iq['pubsub']['subscribe']['options']['options'] = form self.check(iq, """ this thing is awesome """, use_values=False) def testPublish(self): "Testing iq/pubsub/publish stanzas" iq = self.Iq() iq['pubsub']['publish']['node'] = 'thingers' payload = ET.fromstring(""" """) payload2 = ET.fromstring(""" """) item = pubsub.Item() item['id'] = 'asdf' item['payload'] = payload item2 = pubsub.Item() item2['id'] = 'asdf2' item2['payload'] = payload2 iq['pubsub']['publish'].append(item) iq['pubsub']['publish'].append(item2) form = xep_0004.Form() form['type'] = 'submit' form.addField('pubsub#description', ftype='text-single', value='this thing is awesome') iq['pubsub']['publish_options'] = form self.check(iq, """ this thing is awesome """) def testDelete(self): "Testing iq/pubsub_owner/delete stanzas" iq = self.Iq() iq['pubsub_owner']['delete']['node'] = 'thingers' self.check(iq, """ """) def testCreateConfigGet(self): """Testing getting config from full create""" iq = self.Iq() iq['to'] = 'pubsub.asdf' iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7' iq['type'] = 'set' iq['id'] = 'E' pub = iq['pubsub'] pub['create']['node'] = 'testnode2' pub['configure']['form']['type'] = 'submit' pub['configure']['form'].set_fields([ ('FORM_TYPE', {'type': 'hidden', 'value': 'http://jabber.org/protocol/pubsub#node_config'}), ('pubsub#node_type', {'type': 'list-single', 'label': 'Select the node type', 'value': 'leaf'}), ('pubsub#title', {'type': 'text-single', 'label': 'A friendly name for the node'}), ('pubsub#deliver_notifications', {'type': 'boolean', 'label': 'Deliver event notifications', 'value': True}), ('pubsub#deliver_payloads', {'type': 'boolean', 'label': 'Deliver payloads with event notifications', 'value': True}), ('pubsub#notify_config', {'type': 'boolean', 'label': 'Notify subscribers when the node configuration changes'}), ('pubsub#notify_delete', {'type': 'boolean', 'label': 'Notify subscribers when the node is deleted'}), ('pubsub#notify_retract', {'type': 'boolean', 'label': 'Notify subscribers when items are removed from the node', 'value': True}), ('pubsub#notify_sub', {'type': 'boolean', 'label': 'Notify owners about new subscribers and unsubscribes'}), ('pubsub#persist_items', {'type': 'boolean', 'label': 'Persist items in storage'}), ('pubsub#max_items', {'type': 'text-single', 'label': 'Max # of items to persist', 'value': '10'}), ('pubsub#subscribe', {'type': 'boolean', 'label': 'Whether to allow subscriptions', 'value': True}), ('pubsub#access_model', {'type': 'list-single', 'label': 'Specify the subscriber model', 'value': 'open'}), ('pubsub#publish_model', {'type': 'list-single', 'label': 'Specify the publisher model', 'value': 'publishers'}), ('pubsub#send_last_published_item', {'type': 'list-single', 'label': 'Send last published item', 'value': 'never'}), ('pubsub#presence_based_delivery', {'type': 'boolean', 'label': 'Deliver notification only to available users'}), ]) self.check(iq, """ http://jabber.org/protocol/pubsub#node_config leaf 1 1 1 10 1 open publishers never """) def testItemEvent(self): """Testing message/pubsub_event/items/item""" msg = self.Message() item = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) item['payload'] = pl item['id'] = 'abc123' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testItemsEvent(self): """Testing multiple message/pubsub_event/items/item""" msg = self.Message() item = pubsub.EventItem() item2 = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) item2['payload'] = pl2 item['payload'] = pl item['id'] = 'abc123' item2['id'] = '123abc' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items'].append(item2) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testItemsEvent(self): """Testing message/pubsub_event/items/item & retract mix""" msg = self.Message() item = pubsub.EventItem() item2 = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) item2['payload'] = pl2 retract = pubsub.EventRetract() retract['id'] = 'aabbcc' item['payload'] = pl item['id'] = 'abc123' item2['id'] = '123abc' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items'].append(retract) msg['pubsub_event']['items'].append(item2) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testCollectionAssociate(self): """Testing message/pubsub_event/collection/associate""" msg = self.Message() msg['pubsub_event']['collection']['associate']['node'] = 'cheese' msg['pubsub_event']['collection']['node'] = 'cheeseburger' msg['type'] = 'headline' self.check(msg, """ """) def testCollectionDisassociate(self): """Testing message/pubsub_event/collection/disassociate""" msg = self.Message() msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' msg['pubsub_event']['collection']['node'] = 'cheeseburger' msg['type'] = 'headline' self.check(msg, """ """) def testEventConfiguration(self): """Testing message/pubsub_event/configuration/config""" msg = self.Message() msg['pubsub_event']['configuration']['node'] = 'cheese' msg['pubsub_event']['configuration']['form'].addField('pubsub#title', ftype='text-single', value='This thing is awesome') msg['type'] = 'headline' self.check(msg, """ This thing is awesome """) def testEventPurge(self): """Testing message/pubsub_event/purge""" msg = self.Message() msg['pubsub_event']['purge']['node'] = 'pickles' msg['type'] = 'headline' self.check(msg, """ """) def testEventSubscription(self): """Testing message/pubsub_event/subscription""" msg = self.Message() msg['pubsub_event']['subscription']['node'] = 'pickles' msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' msg['pubsub_event']['subscription']['subid'] = 'aabb1122' msg['pubsub_event']['subscription']['subscription'] = 'subscribed' msg['pubsub_event']['subscription']['expiry'] = 'presence' msg['type'] = 'headline' self.check(msg, """ """) def testPubsubError(self): """Test getting a pubsub specific condition from an error stanza""" iq = self.Iq() iq['error']['type'] = 'cancel' iq['error']['code'] = '501' iq['error']['condition'] = 'feature-not-implemented' iq['error']['pubsub']['condition'] = 'subid-required' self.check(iq, """ """, use_values=False) del iq['error']['pubsub']['condition'] self.check(iq, """ """, use_values=False) def testPubsubUnsupportedError(self): """Test getting the feature from an unsupported error""" iq = self.Iq() iq['error']['type'] = 'cancel' iq['error']['code'] = '501' iq['error']['condition'] = 'feature-not-implemented' iq['error']['pubsub']['condition'] = 'unsupported' iq['error']['pubsub']['unsupported'] = 'instant-node' self.check(iq, """ """, use_values=False) self.assertEqual(iq['error']['pubsub']['condition'], 'unsupported') self.assertEqual(iq['error']['pubsub']['unsupported'], 'instant-node') del iq['error']['pubsub']['unsupported'] self.check(iq, """ """, use_values=False) suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas) slixmpp-1.2.2/tests/test_stream_xep_0323.py0000644000175000001440000012734112603534742021603 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- import sys import datetime import time import threading from slixmpp.test import * from slixmpp.xmlstream import ElementBase from slixmpp.plugins.xep_0323.device import Device class TestStreamSensorData(SlixTest): """ Test using the XEP-0323 plugin. """ def setUp(self): pass def _time_now(self): return datetime.datetime.now().replace(microsecond=0).isoformat() def tearDown(self): self.stream_close() def testRequestAccept(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device22") myDevice._add_field(name="Temperature", typename="numeric", unit="°C") myDevice._set_momentary_timestamp("2013-03-07T16:24:30") myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) self.recv(""" """) self.send(""" """) self.send(""" """) def testRequestRejectAuth(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) self.xmpp['xep_0323']._set_authenticated("darth@deathstar.com") self.recv(""" """) self.send(""" Access denied """) def testRequestNode(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" Invalid nodeId Device33 """) print("."), self.recv(""" """) self.send(""" """) def testRequestField(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" Invalid field Current """) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testRequestMultiTimestampSingleField(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field(name='Current', typename="numeric", unit="A") myDevice._add_field(name='Height', typename="string") myDevice._add_field_timestamp_data(name="Voltage", value="230.6", timestamp="2000-01-01T01:01:02") myDevice._add_field_timestamp_data(name="Height", value="115 m", timestamp="2000-01-01T01:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testRequestMultiTimestampAllFields(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.4", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field(name='Current', typename="numeric", unit="A") myDevice._add_field(name='Height', typename="string") myDevice._add_field_timestamp_data(name="Voltage", value="230.6", timestamp="2000-01-01T01:01:02") myDevice._add_field_timestamp_data(name="Height", value="115 m", timestamp="2000-01-01T01:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testRequestAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", callback=None) self.send(""" """) self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=None) self.send(""" """) self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", fields=['Temperature', 'Voltage'], callback=None) self.send(""" """) def testRequestRejectAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): if (result == "rejected") and (error_msg == "Invalid device Device22"): results.append("rejected") self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=my_callback) self.send(""" """) self.recv(""" Invalid device Device22 """) self.failUnless(results == ["rejected"], "Rejected callback was not properly executed") def testRequestAcceptedAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): results.append(result) self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33', 'Device22'], callback=my_callback) self.send(""" """) self.recv(""" """) self.failUnless(results == ["accepted"], "Accepted callback was not properly executed") def testRequestFieldsAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] callback_data = {} def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): results.append(result) if result == "fields": callback_data["nodeId"] = nodeId callback_data["timestamp"] = timestamp callback_data["error_msg"] = error_msg for f in fields: callback_data["field_" + f['name']] = f self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback) #self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback); self.send(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.failUnlessEqual(results, ["accepted","fields","done"]) # self.assertIn("nodeId", callback_data); self.assertTrue("nodeId" in callback_data) self.failUnlessEqual(callback_data["nodeId"], "Device33") # self.assertIn("timestamp", callback_data); self.assertTrue("timestamp" in callback_data) self.failUnlessEqual(callback_data["timestamp"], "2000-01-01T00:01:02") #self.assertIn("field_Voltage", callback_data); self.assertTrue("field_Voltage" in callback_data) self.failUnlessEqual(callback_data["field_Voltage"], {"name": "Voltage", "value": "230.4", "typename": "numeric", "unit": "V", "flags": {"invoiced": "true"}}) #self.assertIn("field_TestBool", callback_data); self.assertTrue("field_TestBool" in callback_data) self.failUnlessEqual(callback_data["field_TestBool"], {"name": "TestBool", "value": "true", "typename": "boolean" }) def testServiceDiscoveryClient(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) self.recv(""" """) self.send(""" """) def testServiceDiscoveryComponent(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) self.recv(""" """) self.send(""" """) def testRequestTimeout(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] callback_data = {} def my_callback(from_jid, result, nodeId=None, timestamp=None, error_msg=None): results.append(result) if result == "failure": callback_data["nodeId"] = nodeId callback_data["timestamp"] = timestamp callback_data["error_msg"] = error_msg self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback) self.send(""" """) self.recv(""" """) self.recv(""" Timeout. """) self.failUnlessEqual(results, ["accepted","failure"]); # self.assertIn("nodeId", callback_data); self.assertTrue("nodeId" in callback_data) self.failUnlessEqual(callback_data["nodeId"], "Device33") # self.assertIn("timestamp", callback_data); self.assertTrue("timestamp" in callback_data) self.failUnlessEqual(callback_data["timestamp"], "2013-03-07T17:13:30") # self.assertIn("error_msg", callback_data); self.assertTrue("error_msg" in callback_data) self.failUnlessEqual(callback_data["error_msg"], "Timeout.") def testDelayedRequest(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device22") myDevice._add_field(name="Temperature", typename="numeric", unit="°C") myDevice._set_momentary_timestamp("2013-03-07T16:24:30") myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) dtnow = datetime.datetime.now() ts_2sec = datetime.timedelta(0,2) dtnow_plus_2sec = dtnow + ts_2sec when_flag = dtnow_plus_2sec.replace(microsecond=0).isoformat() self.recv(""" """) self.send(""" """) time.sleep(1) self.send(""" """) self.send(""" """) def testDelayedRequestFail(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device22") myDevice._add_field(name="Temperature", typename="numeric", unit="°C") myDevice._set_momentary_timestamp("2013-03-07T16:24:30") myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) dtnow = datetime.datetime.now() ts_2sec = datetime.timedelta(0,2) dtnow_minus_2sec = dtnow - ts_2sec when_flag = dtnow_minus_2sec.replace(microsecond=0).isoformat() self.recv(""" """) # Remove the returned datetime to allow predictable test xml_stanza = self._filtered_stanza_prepare() error_text = xml_stanza['rejected']['error'] #['text'] error_text = error_text[:error_text.find(':')] xml_stanza['rejected']['error'] = error_text self._filtered_stanza_check(""" Invalid datetime in 'when' flag, cannot set a time in the past. Current time """, xml_stanza) def _filtered_stanza_prepare(self, timeout=.5): sent = self.xmpp.socket.next_sent(timeout) if sent is None: self.fail("No stanza was sent.") xml = self.parse_xml(sent) self.fix_namespaces(xml, 'jabber:client') sent = self.xmpp._build_stanza(xml, 'jabber:client') return sent def _filtered_stanza_check(self, data, filtered, defaults=None, use_values=True, method='exact'): self.check(filtered, data, method=method, defaults=defaults, use_values=use_values) def testRequestFieldFrom(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testRequestFieldTo(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testRequestFieldFromTo(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device44") myDevice._add_field(name='Voltage', typename="numeric", unit="V") myDevice._add_field_timestamp_data(name="Voltage", value="230.1", timestamp="2000-01-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.2", timestamp="2000-02-01T00:01:02", flags={"invoiced": "true"}) myDevice._add_field_timestamp_data(name="Voltage", value="230.3", timestamp="2000-03-01T00:01:02", flags={"invoiced": "true"}) self.xmpp['xep_0323'].register_node('Device44', myDevice, commTimeout=0.5) print("."), self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) def testDelayedRequestClient(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] callback_data = {} def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): results.append(result) if result == "fields": callback_data["nodeId"] = nodeId callback_data["timestamp"] = timestamp callback_data["error_msg"] = error_msg for f in fields: callback_data["field_" + f['name']] = f self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback) #self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback); self.send(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.failUnlessEqual(results, ["queued","started","fields","done"]); # self.assertIn("nodeId", callback_data); self.assertTrue("nodeId" in callback_data) self.failUnlessEqual(callback_data["nodeId"], "Device33") # self.assertIn("timestamp", callback_data); self.assertTrue("timestamp" in callback_data) self.failUnlessEqual(callback_data["timestamp"], "2000-01-01T00:01:02") # self.assertIn("field_Voltage", callback_data); self.assertTrue("field_Voltage" in callback_data) self.failUnlessEqual(callback_data["field_Voltage"], {"name": "Voltage", "value": "230.4", "typename": "numeric", "unit": "V", "flags": {"invoiced": "true"}}) # self.assertIn("field_TestBool", callback_data); self.assertTrue("field_TestBool" in callback_data) self.failUnlessEqual(callback_data["field_TestBool"], {"name": "TestBool", "value": "true", "typename": "boolean" }) def testRequestFieldsCancelAPI(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0323']) results = [] def my_callback(from_jid, result, nodeId=None, timestamp=None, fields=None, error_msg=None): results.append(result) session = self.xmpp['xep_0323'].request_data(from_jid="tester@localhost", to_jid="you@google.com", nodeIds=['Device33'], callback=my_callback) self.send(""" """) self.recv(""" """) self.xmpp['xep_0323'].cancel_request(session=session) self.send(""" """) self.recv(""" """) self.failUnlessEqual(results, ["accepted","cancelled"]) def testDelayedRequestCancel(self): self.stream_start(mode='component', plugins=['xep_0030', 'xep_0323']) myDevice = Device("Device22") myDevice._add_field(name="Temperature", typename="numeric", unit="°C") myDevice._set_momentary_timestamp("2013-03-07T16:24:30") myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) self.xmpp['xep_0323'].register_node(nodeId="Device22", device=myDevice, commTimeout=0.5) dtnow = datetime.datetime.now() ts_2sec = datetime.timedelta(0,2) dtnow_plus_2sec = dtnow + ts_2sec when_flag = dtnow_plus_2sec.replace(microsecond=0).isoformat() self.recv(""" """) self.send(""" """) self.recv(""" """) self.send(""" """) # Test cancel of non-existing request self.recv(""" """) self.send(""" Cancel request received, no matching request is active. """) # Ensure we don't get anything after cancellation self.send(None) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSensorData) slixmpp-1.2.2/tests/test_stanza_xep_0323.py0000644000175000001440000003370312603534742021606 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- from slixmpp.test import * import slixmpp.plugins.xep_0323 as xep_0323 namespace='sn' class TestSensorDataStanzas(SlixTest): def setUp(self): pass #register_stanza_plugin(Iq, xep_0323.stanza.Request) #register_stanza_plugin(Iq, xep_0323.stanza.Accepted) #register_stanza_plugin(Message, xep_0323.stanza.Failure) #register_stanza_plugin(xep_0323.stanza.Failure, xep_0323.stanza.Error) #register_stanza_plugin(Iq, xep_0323.stanza.Rejected) #register_stanza_plugin(Message, xep_0323.stanza.Fields) #register_stanza_plugin(Message, xep_0323.stanza.Request) #register_stanza_plugin(Message, xep_0323.stanza.Accepted) #register_stanza_plugin(Message, xep_0323.stanza.Failure) # register_stanza_plugin(Message, xep_0323.stanza.Result) # register_stanza_plugin(Message, xep_0323.stanza.Gone) # register_stanza_plugin(Message, xep_0323.stanza.Inactive) # register_stanza_plugin(Message, xep_0323.stanza.Paused) def testRequest(self): """ test of request stanza """ iq = self.Iq() iq['type'] = 'get' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '1' iq['req']['seqnr'] = '1' iq['req']['momentary'] = 'true' self.check(iq,""" """ ) def testRequestNodes(self): """ test of request nodes stanza """ iq = self.Iq() iq['type'] = 'get' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '1' iq['req']['seqnr'] = '1' iq['req']['momentary'] = 'true' iq['req'].add_node("Device02", "Source02", "CacheType") iq['req'].add_node("Device44") self.check(iq,""" """ ) iq['req'].del_node("Device02") self.check(iq,""" """ ) iq['req'].del_nodes() self.check(iq,""" """ ) def testRequestField(self): """ test of request field stanza """ iq = self.Iq() iq['type'] = 'get' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '1' iq['req']['seqnr'] = '1' iq['req']['momentary'] = 'true' iq['req'].add_field("Top temperature") iq['req'].add_field("Bottom temperature") self.check(iq,""" """ ) iq['req'].del_field("Top temperature") self.check(iq,""" """ ) iq['req'].del_fields() self.check(iq,""" """ ) def testAccepted(self): """ test of request stanza """ iq = self.Iq() iq['type'] = 'result' iq['from'] = 'device@clayster.com' iq['to'] = 'master@clayster.com/amr' iq['id'] = '2' iq['accepted']['seqnr'] = '2' self.check(iq,""" """ ) def testRejected(self): """ test of request stanza """ iq = self.Iq() iq['type'] = 'error' iq['from'] = 'device@clayster.com' iq['to'] = 'master@clayster.com/amr' iq['id'] = '4' iq['rejected']['seqnr'] = '4' iq['rejected']['error'] = 'Access denied.' self.check(iq,""" Access denied. """ ) def testFailure(self): """ test of failure stanza """ msg = self.Message() msg['from'] = 'device@clayster.com' msg['to'] = 'master@clayster.com/amr' msg['failure']['seqnr'] = '3' msg['failure']['done'] = 'true' msg['failure']['error']['nodeId'] = 'Device01' msg['failure']['error']['timestamp'] = '2013-03-07T17:13:30' msg['failure']['error']['text'] = 'Timeout.' self.check(msg,""" Timeout. """ ) def testFields(self): """ test of fields stanza """ msg = self.Message() msg['from'] = 'device@clayster.com' msg['to'] = 'master@clayster.com/amr' msg['fields']['seqnr'] = '1' node = msg['fields'].add_node("Device02") ts = node.add_timestamp("2013-03-07T16:24:30") data = ts.add_data(typename="numeric", name="Temperature", value="-12.42", unit='K') data['momentary'] = 'true' data['automaticReadout'] = 'true' self.check(msg,""" """ ) node = msg['fields'].add_node("EmptyDevice") node = msg['fields'].add_node("Device04") ts = node.add_timestamp("EmptyTimestamp") self.check(msg,""" """ ) node = msg['fields'].add_node("Device77") ts = node.add_timestamp("2013-05-03T12:00:01") data = ts.add_data(typename="numeric", name="Temperature", value="-12.42", unit='K') data['historicalDay'] = 'true' data = ts.add_data(typename="numeric", name="Speed", value="312.42", unit='km/h') data['historicalWeek'] = 'false' data = ts.add_data(typename="string", name="Temperature name", value="Bottom oil") data['historicalMonth'] = 'true' data = ts.add_data(typename="string", name="Speed name", value="Top speed") data['historicalQuarter'] = 'false' data = ts.add_data(typename="dateTime", name="T1", value="1979-01-01T00:00:00") data['historicalYear'] = 'true' data = ts.add_data(typename="dateTime", name="T2", value="2000-01-01T01:02:03") data['historicalOther'] = 'false' data = ts.add_data(typename="timeSpan", name="TS1", value="P5Y") data['missing'] = 'true' data = ts.add_data(typename="timeSpan", name="TS2", value="PT2M1S") data['manualEstimate'] = 'false' data = ts.add_data(typename="enum", name="top color", value="red", dataType="string") data['invoiced'] = 'true' data = ts.add_data(typename="enum", name="bottom color", value="black", dataType="string") data['powerFailure'] = 'false' data = ts.add_data(typename="boolean", name="Temperature real", value="false") data['historicalDay'] = 'true' data = ts.add_data(typename="boolean", name="Speed real", value="true") data['historicalWeek'] = 'false' self.check(msg,""" """ ) def testTimestamp(self): msg = self.Message() msg['from'] = 'device@clayster.com' msg['to'] = 'master@clayster.com/amr' msg['fields']['seqnr'] = '1' node = msg['fields'].add_node("Device02") node = msg['fields'].add_node("Device03") ts = node.add_timestamp("2013-03-07T16:24:30") ts = node.add_timestamp("2013-03-07T16:24:31") self.check(msg,""" """ ) def testStringIdsMatcher(self): """ test of StringIds follow spec """ emptyStringIdXML='' msg = self.Message() msg['fields']['stringIds'] = "Nisse" self.check(msg,emptyStringIdXML) msg['fields']['stringIds'] = "Nisse___nje#" self.check(msg,emptyStringIdXML) msg['fields']['stringIds'] = "1" self.check(msg,emptyStringIdXML) suite = unittest.TestLoader().loadTestsFromTestCase(TestSensorDataStanzas) slixmpp-1.2.2/tests/test_stream_presence.py0000644000175000001440000002442013004224717022130 0ustar mathieuiusers00000000000000import time import unittest from slixmpp.test import SlixTest class TestStreamPresence(SlixTest): """ Test handling roster updates. """ def setUp(self): self.stream_start(jid='tester@localhost', plugins=[]) def tearDown(self): self.stream_close() def testInitialUnavailablePresences(self): """ Test receiving unavailable presences from JIDs that are not online. """ events = set() def got_offline(presence): # The got_offline event should not be triggered. events.add('got_offline') def unavailable(presence): # The presence_unavailable event should be triggered. events.add('unavailable') self.xmpp.add_event_handler('got_offline', got_offline) self.xmpp.add_event_handler('presence_unavailable', unavailable) self.recv(""" """) self.assertEqual(events, {'unavailable'}, "Got offline incorrectly triggered: %s." % events) def testGotOffline(self): """Test that got_offline is triggered properly.""" events = [] def got_offline(presence): events.append('got_offline') self.xmpp.add_event_handler('got_offline', got_offline) # Setup roster. Use a 'set' instead of 'result' so we # don't have to handle get_roster() blocking. # # We use the stream to initialize the roster to make # the test independent of the roster implementation. self.recv(""" Testers """) # Contact comes online. self.recv(""" """) # Contact goes offline, should trigger got_offline. self.recv(""" """) self.assertEqual(events, ['got_offline'], "Got offline incorrectly triggered: %s" % events) def testGotOnline(self): """Test that got_online is triggered properly.""" events = set() def presence_available(p): events.add('presence_available') def got_online(p): events.add('got_online') self.xmpp.add_event_handler('presence_available', presence_available) self.xmpp.add_event_handler('got_online', got_online) self.recv(""" """) expected = {'presence_available', 'got_online'} self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) def testAutoAuthorizeAndSubscribe(self): """ Test auto authorizing and auto subscribing to subscription requests. """ events = set() def presence_subscribe(p): events.add('presence_subscribe') def changed_subscription(p): events.add('changed_subscription') self.xmpp.add_event_handler('changed_subscription', changed_subscription) self.xmpp.add_event_handler('presence_subscribe', presence_subscribe) # With these settings we should accept a subscription # and request a subscription in return. self.xmpp.auto_authorize = True self.xmpp.auto_subscribe = True self.recv(""" """) self.send(""" """) self.send(""" """) self.send(""" """) expected = {'presence_subscribe', 'changed_subscription'} self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) def testNoAutoAuthorize(self): """Test auto rejecting subscription requests.""" events = set() def presence_subscribe(p): events.add('presence_subscribe') def changed_subscription(p): events.add('changed_subscription') self.xmpp.add_event_handler('changed_subscription', changed_subscription) self.xmpp.add_event_handler('presence_subscribe', presence_subscribe) # With this setting we should reject all subscriptions. self.xmpp.roster['tester@localhost'].auto_authorize = False self.recv(""" """) self.send(""" """) expected = {'presence_subscribe', 'changed_subscription'} self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) def test_presence_events(self): """Test that presence events are raised.""" events = [] ptypes = ['available', 'away', 'dnd', 'xa', 'chat', 'unavailable', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'] for ptype in ptypes: handler = lambda p: events.append(p['type']) self.xmpp.add_event_handler('presence_%s' % ptype, handler) self.recv(""" """) self.recv(""" away """) self.recv(""" dnd """) self.recv(""" xa """) self.recv(""" chat """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) 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 = [] def changed_status(presence): events.append(presence['type']) self.xmpp.add_event_handler('changed_status', changed_status) self.recv(""" """) self.recv(""" """) self.recv(""" away """) self.recv(""" away """) self.recv(""" dnd """) self.recv(""" dnd """) self.recv(""" chat """) self.recv(""" chat """) self.recv(""" xa """) self.recv(""" xa """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) # Changed status text, so fire new event self.recv(""" Testing! """) # No change in show/status values, no event self.recv(""" Testing! """) self.recv(""" dnd Testing! """) self.recv(""" dnd Testing! """) self.assertEqual(events, ['available', 'away', 'dnd', 'chat', 'xa', 'unavailable', 'available', 'available', 'dnd'], "Changed status events incorrect: %s" % events) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence) slixmpp-1.2.2/tests/test_stanza_element.py0000644000175000001440000010773613004224717021776 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream.stanzabase import ElementBase, register_stanza_plugin, ET from collections import OrderedDict class TestElementBase(SlixTest): def testFixNs(self): """Test fixing namespaces in an XPath expression.""" e = ElementBase() ns = "http://jabber.org/protocol/disco#items" result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns)) expected = "/".join(["{%s}foo" % ns, "{%s}bar" % ns, "{abc}baz", "{%s}more" % ns]) self.failUnless(expected == result, "Incorrect namespace fixing result: %s" % str(result)) def testExtendedName(self): """Test element names of the form tag1/tag2/tag3.""" class TestStanza(ElementBase): name = "foo/bar/baz" namespace = "test" stanza = TestStanza() self.check(stanza, """ """) def testGetStanzaValues(self): """Test get_stanza_values using plugins and substanzas.""" class TestStanzaPlugin(ElementBase): name = "foo2" namespace = "foo" interfaces = {'bar', 'baz'} plugin_attrib = "foo2" class TestSubStanza(ElementBase): name = "subfoo" namespace = "foo" interfaces = {'bar', 'baz'} class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} register_stanza_plugin(TestStanza, TestStanzaPlugin, iterable=True) stanza = TestStanza() stanza['bar'] = 'a' stanza['foo2']['baz'] = 'b' substanza = TestSubStanza() substanza['bar'] = 'c' stanza.append(substanza) values = stanza.get_stanza_values() expected = {'lang': '', 'bar': 'a', 'baz': '', 'foo2': {'lang': '', 'bar': '', 'baz': 'b'}, 'substanzas': [{'__childtag__': '{foo}foo2', 'lang': '', 'bar': '', 'baz': 'b'}, {'__childtag__': '{foo}subfoo', 'lang': '', 'bar': 'c', 'baz': ''}]} self.failUnless(values == expected, "Unexpected stanza values:\n%s\n%s" % (str(expected), str(values))) def testSetStanzaValues(self): """Test using set_stanza_values with substanzas and plugins.""" class TestStanzaPlugin(ElementBase): name = "pluginfoo" namespace = "foo" interfaces = {'bar', 'baz'} plugin_attrib = "plugin_foo" class TestStanzaPlugin2(ElementBase): name = "pluginfoo2" namespace = "foo" interfaces = {'bar', 'baz'} plugin_attrib = "plugin_foo2" class TestSubStanza(ElementBase): name = "subfoo" namespace = "foo" interfaces = {'bar', 'baz'} class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) register_stanza_plugin(TestStanza, TestStanzaPlugin2) stanza = TestStanza() values = {'bar': 'a', 'baz': '', 'plugin_foo': {'bar': '', 'baz': 'b'}, 'plugin_foo2': {'bar': 'd', 'baz': 'e'}, 'substanzas': [{'__childtag__': '{foo}subfoo', 'bar': 'c', 'baz': ''}]} stanza.values = values self.check(stanza, """ """) def testGetItem(self): """Test accessing stanza interfaces.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz', 'qux'} sub_interfaces = {'baz'} def get_qux(self): return 'qux' class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = {'fizz'} register_stanza_plugin(TestStanza, TestStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() substanza = TestStanza() stanza.append(substanza) stanza.set_stanza_values({'bar': 'a', 'baz': 'b', 'qux': 42, 'foobar': {'fizz': 'c'}}) # Test non-plugin interfaces expected = {'substanzas': [substanza], 'bar': 'a', 'baz': 'b', 'qux': 'qux', 'meh': ''} for interface, value in expected.items(): result = stanza[interface] self.failUnless(result == value, "Incorrect stanza interface access result: %s" % result) # Test plugin interfaces self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin), "Incorrect plugin object result.") self.failUnless(stanza['foobar']['fizz'] == 'c', "Incorrect plugin subvalue result.") def testSetItem(self): """Test assigning to stanza interfaces.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz', 'qux'} sub_interfaces = {'baz'} def set_qux(self, value): pass class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = {'foobar'} register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() stanza['bar'] = 'attribute!' stanza['baz'] = 'element!' stanza['qux'] = 'overridden' stanza['foobar'] = 'plugin' self.check(stanza, """ element! """) def testDelItem(self): """Test deleting stanza interface values.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz', 'qux'} sub_interfaces = {'bar'} def del_qux(self): pass class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = {'foobar'} register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' stanza['qux'] = 'c' stanza['foobar']['foobar'] = 'd' self.check(stanza, """ a """) del stanza['bar'] del stanza['baz'] del stanza['qux'] del stanza['foobar'] self.check(stanza, """ """) def testModifyingAttributes(self): """Test modifying top level attributes of a stanza's XML object.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} stanza = TestStanza() self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar') == '', "Incorrect value returned for an unset XML attribute.") stanza._set_attr('bar', 'a') stanza._set_attr('baz', 'b') self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar') == 'a', "Retrieved XML attribute value is incorrect.") stanza._set_attr('bar', None) stanza._del_attr('baz') self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar', 'c') == 'c', "Incorrect default value returned for an unset XML attribute.") def testGetSubText(self): """Test retrieving the contents of a sub element.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar'} def set_bar(self, value): wrapper = ET.Element("{foo}wrapper") bar = ET.Element("{foo}bar") bar.text = value wrapper.append(bar) self.xml.append(wrapper) def get_bar(self): return self._get_sub_text("wrapper/bar", default="not found") stanza = TestStanza() self.failUnless(stanza['bar'] == 'not found', "Default _get_sub_text value incorrect.") stanza['bar'] = 'found' self.check(stanza, """ found """) self.failUnless(stanza['bar'] == 'found', "_get_sub_text value incorrect: %s." % stanza['bar']) def testSubElement(self): """Test setting the contents of a sub element.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} def set_baz(self, value): self._set_sub_text("wrapper/baz", text=value) def get_baz(self): return self._get_sub_text("wrapper/baz") def set_bar(self, value): self._set_sub_text("wrapper/bar", text=value) def get_bar(self): return self._get_sub_text("wrapper/bar") stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' self.check(stanza, """ a b """) stanza._set_sub_text('wrapper/bar', text='', keep=True) self.check(stanza, """ b """, use_values=False) stanza['bar'] = 'a' stanza._set_sub_text('wrapper/bar', text='') self.check(stanza, """ b """) def testDelSub(self): """Test removing sub elements.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} def set_bar(self, value): self._set_sub_text("path/to/only/bar", value) def get_bar(self): return self._get_sub_text("path/to/only/bar") def del_bar(self): self._del_sub("path/to/only/bar") def set_baz(self, value): self._set_sub_text("path/to/just/baz", value) def get_baz(self): return self._get_sub_text("path/to/just/baz") def del_baz(self): self._del_sub("path/to/just/baz") stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' self.check(stanza, """ a b """) del stanza['bar'] del stanza['baz'] self.check(stanza, """ """, use_values=False) stanza['bar'] = 'a' stanza['baz'] = 'b' stanza._del_sub('path/to/only/bar', all=True) self.check(stanza, """ b """) def testMatch(self): """Test matching a stanza against an XPath expression.""" class TestSubStanza(ElementBase): name = "sub" namespace = "baz" interfaces = {'attrib'} class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar','baz', 'qux'} sub_interfaces = {'qux'} def set_qux(self, value): self._set_sub_text('qux', text=value) def get_qux(self): return self._get_sub_text('qux') class TestStanzaPlugin(ElementBase): name = "plugin" namespace = "http://test/slash/bar" interfaces = {'attrib'} register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() self.failUnless(stanza.match("foo"), "Stanza did not match its own tag name.") self.failUnless(stanza.match("{foo}foo"), "Stanza did not match its own namespaced name.") stanza['bar'] = 'a' self.failUnless(stanza.match("foo@bar=a"), "Stanza did not match its own name with attribute value check.") stanza['baz'] = 'b' self.failUnless(stanza.match("foo@bar=a@baz=b"), "Stanza did not match its own name with multiple attributes.") stanza['qux'] = 'c' self.failUnless(stanza.match("foo/qux"), "Stanza did not match with subelements.") stanza['qux'] = '' self.failUnless(stanza.match("foo/qux") == False, "Stanza matched missing subinterface element.") self.failUnless(stanza.match("foo/bar") == False, "Stanza matched nonexistent element.") stanza['plugin']['attrib'] = 'c' self.failUnless(stanza.match("foo/plugin@attrib=c"), "Stanza did not match with plugin and attribute.") self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"), "Stanza did not match with namespaced plugin.") substanza = TestSubStanza() substanza['attrib'] = 'd' stanza.append(substanza) self.failUnless(stanza.match("foo/sub@attrib=d"), "Stanza did not match with substanzas and attribute.") self.failUnless(stanza.match("foo/{baz}sub"), "Stanza did not match with namespaced substanza.") def testComparisons(self): """Test comparing ElementBase objects.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} stanza1 = TestStanza() stanza1['bar'] = 'a' self.failUnless(stanza1, "Stanza object does not evaluate to True") stanza2 = TestStanza() stanza2['baz'] = 'b' self.failUnless(stanza1 != stanza2, "Different stanza objects incorrectly compared equal.") stanza1['baz'] = 'b' stanza2['bar'] = 'a' self.failUnless(stanza1 == stanza2, "Equal stanzas incorrectly compared inequal.") def testKeys(self): """Test extracting interface names from a stanza object.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} plugin_attrib = 'qux' register_stanza_plugin(TestStanza, TestStanza) stanza = TestStanza() self.failUnless(set(stanza.keys()) == {'lang', 'bar', 'baz'}, "Returned set of interface keys does not match expected.") stanza.enable('qux') self.failUnless(set(stanza.keys()) == {'lang', 'bar', 'baz', 'qux'}, "Incorrect set of interface and plugin keys.") def testGet(self): """Test accessing stanza interfaces using get().""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} stanza = TestStanza() stanza['bar'] = 'a' self.failUnless(stanza.get('bar') == 'a', "Incorrect value returned by stanza.get") self.failUnless(stanza.get('baz', 'b') == 'b', "Incorrect default value returned by stanza.get") def testSubStanzas(self): """Test manipulating substanzas of a stanza object.""" class TestSubStanza(ElementBase): name = "foobar" namespace = "foo" interfaces = {'qux'} class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) stanza = TestStanza() substanza1 = TestSubStanza() substanza2 = TestSubStanza() substanza1['qux'] = 'a' substanza2['qux'] = 'b' # Test appending substanzas self.failUnless(len(stanza) == 0, "Incorrect empty stanza size.") stanza.append(substanza1) self.check(stanza, """ """, use_values=False) self.failUnless(len(stanza) == 1, "Incorrect stanza size with 1 substanza.") stanza.append(substanza2) self.check(stanza, """ """, use_values=False) self.failUnless(len(stanza) == 2, "Incorrect stanza size with 2 substanzas.") # Test popping substanzas stanza.pop(0) self.check(stanza, """ """, use_values=False) # Test iterating over substanzas stanza.append(substanza1) results = [] for substanza in stanza: results.append(substanza['qux']) self.failUnless(results == ['b', 'a'], "Iteration over substanzas failed: %s." % str(results)) def testCopy(self): """Test copying stanza objects.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} stanza1 = TestStanza() stanza1['bar'] = 'a' stanza2 = stanza1.__copy__() self.failUnless(stanza1 == stanza2, "Copied stanzas are not equal to each other.") stanza1['baz'] = 'b' self.failUnless(stanza1 != stanza2, "Divergent stanza copies incorrectly compared equal.") def testExtension(self): """Testing using is_extension.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} class TestExtension(ElementBase): name = 'extended' namespace = 'foo' plugin_attrib = name interfaces = {name} is_extension = True def set_extended(self, value): self.xml.text = value def get_extended(self): return self.xml.text def del_extended(self): self.parent().xml.remove(self.xml) register_stanza_plugin(TestStanza, TestExtension) stanza = TestStanza() stanza['extended'] = 'testing' self.check(stanza, """ testing """) self.failUnless(stanza['extended'] == 'testing', "Could not retrieve stanza extension value.") del stanza['extended'] self.check(stanza, """ """) def testOverrides(self): """Test using interface overrides.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar', 'baz'} class TestOverride(ElementBase): name = 'overrider' namespace = 'foo' plugin_attrib = name interfaces = {'bar'} overrides = ['set_bar'] def setup(self, xml): # Don't create XML for the plugin self.xml = ET.Element('') def set_bar(self, value): if not value.startswith('override-'): self.parent()._set_attr('bar', 'override-%s' % value) else: self.parent()._set_attr('bar', value) stanza = TestStanza() stanza['bar'] = 'foo' self.check(stanza, """ """) register_stanza_plugin(TestStanza, TestOverride, overrides=True) stanza = TestStanza() stanza['bar'] = 'foo' self.check(stanza, """ """) def testBoolInterfaces(self): """Test using boolean interfaces.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = {'bar'} bool_interfaces = interfaces stanza = TestStanza() self.check(stanza, """ """) self.assertFalse(stanza['bar'], "Returned True for missing bool interface element.") stanza['bar'] = True self.check(stanza, """ """) self.assertTrue(stanza['bar'], "Returned False for present bool interface element.") stanza['bar'] = False self.check(stanza, """ """) def testGetMultiAttrib(self): """Test retrieving multi_attrib substanzas.""" class TestStanza(ElementBase): name = 'foo' namespace = 'foo' interfaces = set() class TestMultiStanza1(ElementBase): name = 'bar' namespace = 'bar' plugin_attrib = name plugin_multi_attrib = 'bars' class TestMultiStanza2(ElementBase): name = 'baz' namespace = 'baz' plugin_attrib = name plugin_multi_attrib = 'bazs' register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True) register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True) stanza = TestStanza() stanza.append(TestMultiStanza1()) stanza.append(TestMultiStanza2()) stanza.append(TestMultiStanza1()) stanza.append(TestMultiStanza2()) self.check(stanza, """ """, use_values=False) bars = stanza['bars'] bazs = stanza['bazs'] for bar in bars: self.check(bar, """ """) for baz in bazs: self.check(baz, """ """) self.assertEqual(len(bars), 2, "Wrong number of stanzas: %s" % len(bars)) self.assertEqual(len(bazs), 2, "Wrong number of stanzas: %s" % len(bazs)) def testSetMultiAttrib(self): """Test setting multi_attrib substanzas.""" class TestStanza(ElementBase): name = 'foo' namespace = 'foo' interfaces = set() class TestMultiStanza1(ElementBase): name = 'bar' namespace = 'bar' plugin_attrib = name plugin_multi_attrib = 'bars' class TestMultiStanza2(ElementBase): name = 'baz' namespace = 'baz' plugin_attrib = name plugin_multi_attrib = 'bazs' register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True) register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True) stanza = TestStanza() stanza['bars'] = [TestMultiStanza1(), TestMultiStanza1()] stanza['bazs'] = [TestMultiStanza2(), TestMultiStanza2()] self.check(stanza, """ """, use_values=False) self.assertEqual(len(stanza['substanzas']), 4, "Wrong number of substanzas: %s" % len(stanza['substanzas'])) stanza['bars'] = [TestMultiStanza1()] self.check(stanza, """ """, use_values=False) self.assertEqual(len(stanza['substanzas']), 3, "Wrong number of substanzas: %s" % len(stanza['substanzas'])) def testDelMultiAttrib(self): """Test deleting multi_attrib substanzas.""" class TestStanza(ElementBase): name = 'foo' namespace = 'foo' interfaces = set() class TestMultiStanza1(ElementBase): name = 'bar' namespace = 'bar' plugin_attrib = name plugin_multi_attrib = 'bars' class TestMultiStanza2(ElementBase): name = 'baz' namespace = 'baz' plugin_attrib = name plugin_multi_attrib = 'bazs' register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True) register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True) stanza = TestStanza() bars = [TestMultiStanza1(), TestMultiStanza1()] bazs = [TestMultiStanza2(), TestMultiStanza2()] stanza['bars'] = bars stanza['bazs'] = bazs self.check(stanza, """ """, use_values=False) del stanza['bars'] self.check(stanza, """ """, use_values=False) self.assertEqual(len(stanza['substanzas']), 2, "Wrong number of substanzas: %s" % len(stanza['substanzas'])) def testDefaultLang(self): """Test setting a normal subinterface when a default language is set""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['lang'] = 'sv' stanza['test'] = 'hej' self.check(stanza, """ hej """) self.assertEqual(stanza['test'], 'hej', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test|sv'], 'hej', "Incorrect subinterface value: %s" % stanza['test|sv']) def testSpecifyLangWithDefault(self): """Test specifying various languages.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['lang'] = 'sv' stanza['test'] = 'hej' stanza['test|en'] = 'hi' stanza['test|es'] = 'hola' self.check(stanza, """ hej hi hola """) self.assertEqual(stanza['test'], 'hej', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test|sv'], 'hej', "Incorrect subinterface value: %s" % stanza['test|sv']) self.assertEqual(stanza['test|en'], 'hi', "Incorrect subinterface value: %s" % stanza['test|en']) self.assertEqual(stanza['test|es'], 'hola', "Incorrect subinterface value: %s" % stanza['test|es']) def testSpecifyLangWithNoDefault(self): """Test specifying various languages.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['test'] = 'hej' stanza['test|en'] = 'hi' stanza['test|es'] = 'hola' self.check(stanza, """ hej hi hola """) self.assertEqual(stanza['test'], 'hej', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test|en'], 'hi', "Incorrect subinterface value: %s" % stanza['test|en']) self.assertEqual(stanza['test|es'], 'hola', "Incorrect subinterface value: %s" % stanza['test|es']) def testModifyLangInterfaceWithDefault(self): """Test resetting an interface when a default lang is used.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['lang'] = 'es' stanza['test'] = 'hola' stanza['test|en'] = 'hi' self.check(stanza, """ hola hi """) stanza['test'] = 'adios' stanza['test|en'] = 'bye' self.check(stanza, """ adios bye """) self.assertEqual(stanza['test'], 'adios', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test|es'], 'adios', "Incorrect subinterface value: %s" % stanza['test|es']) self.assertEqual(stanza['test|en'], 'bye', "Incorrect subinterface value: %s" % stanza['test|en']) stanza['test|es'] = 'hola' self.check(stanza, """ hola bye """) self.assertEqual(stanza['test'], 'hola', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test|es'], 'hola', "Incorrect subinterface value: %s" % stanza['test|es']) def testModifyLangInterfaceWithNoDefault(self): """Test resetting an interface when no default lang is used.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['test'] = 'hola' stanza['test|en'] = 'hi' self.check(stanza, """ hola hi """) stanza['test'] = 'adios' stanza['test|en'] = 'bye' self.check(stanza, """ adios bye """) self.assertEqual(stanza['test'], 'adios', "Incorrect subinterface value: %s" % stanza['test']) self.assertEqual(stanza['test'], 'adios', "Incorrect subinterface value: %s" % stanza['test|es']) self.assertEqual(stanza['test|en'], 'bye', "Incorrect subinterface value: %s" % stanza['test|en']) def testDelInterfacesWithDefaultLang(self): """Test deleting interfaces with a default lang set.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['lang'] = 'en' stanza['test'] = 'hi' stanza['test|no'] = 'hej' stanza['test|fr'] = 'bonjour' self.check(stanza, """ hi hej bonjour """) del stanza['test'] self.check(stanza, """ hej bonjour """) del stanza['test|no'] self.check(stanza, """ bonjour """) def testDelInterfacesWithNoDefaultLang(self): """Test deleting interfaces with no default lang set.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces stanza = TestStanza() stanza['test'] = 'hi' stanza['test|no'] = 'hej' stanza['test|fr'] = 'bonjour' self.check(stanza, """ hi hej bonjour """) del stanza['test'] self.check(stanza, """ hej bonjour """) del stanza['test|no'] self.check(stanza, """ bonjour """) def testStarLang(self): """Test using interface|*.""" class TestStanza(ElementBase): name = 'foo' namespace = 'test' interfaces = {'test'} sub_interfaces = interfaces lang_interfaces = interfaces data = OrderedDict() data['en'] = 'hi' data['fr'] = 'bonjour' data['no'] = 'hej' stanza = TestStanza() stanza['test|*'] = data self.check(stanza, """ hi bonjour hej """) data2 = stanza['test|*'] self.assertEqual(data, data2, "Did not extract expected language data: %s" % data2) del stanza['test|*'] self.check(stanza, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) slixmpp-1.2.2/tests/test_stanza_xep_0030.py0000644000175000001440000004235113004224717021572 0ustar mathieuiusers00000000000000import unittest from slixmpp import Iq from slixmpp.test import SlixTest import slixmpp.plugins.xep_0030 as xep_0030 from slixmpp.xmlstream import register_stanza_plugin class TestDisco(SlixTest): """ Test creating and manipulating the disco#info and disco#items stanzas from the XEP-0030 plugin. """ def setUp(self): register_stanza_plugin(Iq, xep_0030.DiscoInfo) register_stanza_plugin(Iq, xep_0030.DiscoItems) def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" iq = self.Iq() iq['disco_info']['node'] = '' self.check(iq, """ """) def testCreateInfoQueryWithNode(self): """Testing disco#info query with a node.""" iq = self.Iq() iq['disco_info']['node'] = 'foo' self.check(iq, """ """) def testCreateItemsQueryNoNode(self): """Testing disco#items query with no node.""" iq = self.Iq() iq['disco_items']['node'] = '' self.check(iq, """ """) def testCreateItemsQueryWithNode(self): """Testing disco#items query with a node.""" iq = self.Iq() iq['disco_items']['node'] = 'foo' self.check(iq, """ """) def testIdentities(self): """Testing adding identities to disco#info.""" iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom', lang='en') self.check(iq, """ """) def testDuplicateIdentities(self): """ Test adding multiple copies of the same category and type combination. Only the first identity should be kept. """ iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom') iq['disco_info'].add_identity('conference', 'text', name='MUC') self.check(iq, """ """) def testDuplicateIdentitiesWithLangs(self): """ Test adding multiple copies of the same category, type, and language combination. Only the first identity should be kept. """ iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom', lang='en') iq['disco_info'].add_identity('conference', 'text', name='MUC', lang='en') self.check(iq, """ """) def testRemoveIdentitiesNoLang(self): """Test removing identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'bot') iq['disco_info'].del_identity('client', 'pc') self.check(iq, """ """) def testRemoveIdentitiesWithLang(self): """Test removing identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'bot') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].del_identity('client', 'pc') self.check(iq, """ """) def testRemoveAllIdentitiesNoLang(self): """Test removing all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'bot', name='Bot') iq['disco_info'].add_identity('client', 'bot', lang='no') iq['disco_info'].add_identity('client', 'console') del iq['disco_info']['identities'] self.check(iq, """ """) def testRemoveAllIdentitiesWithLang(self): """Test removing all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'bot', name='Bot') iq['disco_info'].add_identity('client', 'bot', lang='no') iq['disco_info'].add_identity('client', 'console') iq['disco_info'].del_identities(lang='no') self.check(iq, """ """) def testAddBatchIdentitiesNoLang(self): """Test adding multiple identities at once to a disco#info stanza.""" iq = self.Iq() identities = [('client', 'pc', 'no', 'PC Client'), ('client', 'bot', None, 'Bot'), ('client', 'console', None, None)] iq['disco_info']['identities'] = identities self.check(iq, """ """) def testAddBatchIdentitiesWithLang(self): """Test selectively replacing identities based on language.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') identities = [('client', 'bot', 'fr', 'Bot'), ('client', 'bot', 'en', 'Bot')] iq['disco_info'].set_identities(identities, lang='fr') self.check(iq, """ """) def testGetIdentitiesNoLang(self): """Test getting all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') expected = {('client', 'pc', None, None), ('client', 'pc', 'no', None), ('client', 'pc', 'en', None), ('client', 'pc', 'fr', None)} self.failUnless(iq['disco_info']['identities'] == expected, "Identities do not match:\n%s\n%s" % ( expected, iq['disco_info']['identities'])) def testGetIdentitiesWithLang(self): """ Test getting all identities of a given lang from a disco#info stanza. """ iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') expected = {('client', 'pc', 'no', None)} result = iq['disco_info'].get_identities(lang='no') self.failUnless(result == expected, "Identities do not match:\n%s\n%s" % ( expected, result)) def testFeatures(self): """Testing adding features to disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') self.check(iq, """ """) def testFeaturesDuplicate(self): """Test adding duplicate features to disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('foo') self.check(iq, """ """) def testRemoveFeature(self): """Test removing a feature from disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') iq['disco_info'].del_feature('foo') self.check(iq, """ """) def testGetFeatures(self): """Test getting all features from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') expected = {'foo', 'bar', 'baz'} self.failUnless(iq['disco_info']['features'] == expected, "Features do not match:\n%s\n%s" % ( expected, iq['disco_info']['features'])) def testRemoveAllFeatures(self): """Test removing all features from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') del iq['disco_info']['features'] self.check(iq, """ """) def testAddBatchFeatures(self): """Test adding multiple features at once to a disco#info stanza.""" iq = self.Iq() features = ['foo', 'bar', 'baz'] iq['disco_info']['features'] = features self.check(iq, """ """) def testItems(self): """Testing adding features to disco#info.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', 'foo') iq['disco_items'].add_item('user@localhost', 'bar', name='Testing') self.check(iq, """ """) def testDuplicateItems(self): """Test adding items with the same JID without any nodes.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost', name='First') iq['disco_items'].add_item('user@localhost', name='Second') self.check(iq, """ """) def testDuplicateItemsWithNodes(self): """Test adding items with the same JID/node combination.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost', node='foo', name='First') iq['disco_items'].add_item('user@localhost', node='foo', name='Second') self.check(iq, """ """) def testRemoveItemsNoNode(self): """Test removing items without nodes from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost') iq['disco_items'].del_item('user@localhost') self.check(iq, """ """) def testRemoveItemsWithNode(self): """Test removing items with nodes from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost') iq['disco_items'].del_item('user@localhost', node='foo') self.check(iq, """ """) def testGetItems(self): """Test retrieving items from disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost', node='bar', name='Tester') expected = {('user@localhost', None, None), ('user@localhost', 'foo', None), ('test@localhost', 'bar', 'Tester')} self.failUnless(iq['disco_items']['items'] == expected, "Items do not match:\n%s\n%s" % ( expected, iq['disco_items']['items'])) def testRemoveAllItems(self): """Test removing all items from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost', node='bar', name='Tester') del iq['disco_items']['items'] self.check(iq, """ """) def testAddBatchItems(self): """Test adding multiple items to a disco#items stanza.""" iq = self.Iq() items = [('user@localhost', 'foo', 'Test'), ('test@localhost', None, None), ('other@localhost', None, 'Other')] iq['disco_items']['items'] = items self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) slixmpp-1.2.2/tests/test_stream_xep_0050.py0000644000175000001440000006173513004224717021576 0ustar mathieuiusers00000000000000import time import logging import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream import ElementBase, register_stanza_plugin class TestAdHocCommands(SlixTest): def setUp(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0004', 'xep_0050']) # Real session IDs don't make for nice tests, so use # a dummy value. self.xmpp['xep_0050'].new_session = lambda: '_sessionid_' def tearDown(self): self.stream_close() def testInitialPayloadCommand(self): """Test a command with an initial payload.""" class TestPayload(ElementBase): name = 'foo' namespace = 'test' interfaces = {'bar'} plugin_attrib = name Command = self.xmpp['xep_0050'].stanza.Command register_stanza_plugin(Command, TestPayload, iterable=True) def handle_command(iq, session): initial = session['payload'] logging.debug(initial) new_payload = TestPayload() if initial: new_payload['bar'] = 'Received: %s' % initial['bar'] else: new_payload['bar'] = 'Failed' logging.debug(initial) session['payload'] = new_payload session['next'] = None session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) def testZeroStepCommand(self): """Test running a command with no steps.""" def handle_command(iq, session): form = self.xmpp['xep_0004'].make_form(ftype='result') form.addField(var='foo', ftype='text-single', label='Foo', value='bar') session['payload'] = form session['next'] = None session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" bar """) def testOneStepCommand(self): """Test running a single step command.""" results = [] def handle_command(iq, session): def handle_form(form, session): results.append(form.get_values()['foo']) session['payload'] = None form = self.xmpp['xep_0004'].make_form('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_form session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.assertEqual(results, ['blah'], "Command handler was not executed: %s" % results) def testTwoStepCommand(self): """Test using a two-stage command.""" results = [] def handle_command(iq, session): def handle_step2(form, session): results.append(form.get_values()['bar']) session['payload'] = None def handle_step1(form, session): results.append(form.get_values()['foo']) form = self.xmpp['xep_0004'].make_form('form') form.addField(var='bar', ftype='text-single', label='Bar') session['payload'] = form session['next'] = handle_step2 session['has_next'] = False return session form = self.xmpp['xep_0004'].make_form('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_step1 session['has_next'] = True return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.recv(""" meh """) self.send(""" """) self.assertEqual(results, ['blah', 'meh'], "Command handler was not executed: %s" % results) def testCancelCommand(self): """Test canceling command.""" results = [] def handle_command(iq, session): def handle_form(form, session): results.append(form['values']['foo']) def handle_cancel(iq, session): results.append('canceled') form = self.xmpp['xep_0004'].make_form('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_form session['cancel'] = handle_cancel session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.assertEqual(results, ['canceled'], "Cancelation handler not executed: %s" % results) def testCommandNote(self): """Test adding notes to commands.""" def handle_command(iq, session): form = self.xmpp['xep_0004'].make_form(ftype='result') form.addField(var='foo', ftype='text-single', label='Foo', value='bar') session['payload'] = form session['next'] = None session['has_next'] = False session['notes'] = [('info', 'testing notes')] return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" testing notes bar """) def testMultiPayloads(self): """Test using commands with multiple payloads.""" results = [] def handle_command(iq, session): def handle_form(forms, session): for form in forms: results.append(form.get_values()['FORM_TYPE']) session['payload'] = None form1 = self.xmpp['xep_0004'].make_form('form') form1.addField(var='FORM_TYPE', ftype='hidden', value='form_1') form1.addField(var='foo', ftype='text-single', label='Foo') form2 = self.xmpp['xep_0004'].make_form('form') form2.addField(var='FORM_TYPE', ftype='hidden', value='form_2') form2.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = [form1, form2] session['next'] = handle_form session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" form_1 form_2 """) self.recv(""" form_1 bar form_2 bar """) self.send(""" """) self.assertEqual(results, [['form_1'], ['form_2']], "Command handler was not executed: %s" % results) def testClientAPI(self): """Test using client-side API for commands.""" results = [] def handle_complete(iq, session): for item in session['custom_data']: results.append(item) def handle_step2(iq, session): form = self.xmpp['xep_0004'].make_form(ftype='submit') form.addField(var='bar', value='123') session['custom_data'].append('baz') session['payload'] = form session['next'] = handle_complete self.xmpp['xep_0050'].complete_command(session) def handle_step1(iq, session): form = self.xmpp['xep_0004'].make_form(ftype='submit') form.addField(var='foo', value='42') session['custom_data'].append('bar') session['payload'] = form session['next'] = handle_step2 self.xmpp['xep_0050'].continue_command(session) session = {'custom_data': ['foo'], 'next': handle_step1} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.send(""" 42 """) self.recv(""" """) self.send(""" 123 """) self.recv(""" """) self.failUnless(results == ['foo', 'bar', 'baz'], 'Incomplete command workflow: %s' % results) def testClientAPICancel(self): """Test using client-side cancel API for commands.""" results = [] def handle_canceled(iq, session): for item in session['custom_data']: results.append(item) def handle_step1(iq, session): session['custom_data'].append('bar') session['next'] = handle_canceled self.xmpp['xep_0050'].cancel_command(session) session = {'custom_data': ['foo'], 'next': handle_step1} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.send(""" """) self.recv(""" """) self.failUnless(results == ['foo', 'bar'], 'Incomplete command workflow: %s' % results) def testClientAPIError(self): """Test using client-side error API for commands.""" results = [] def handle_error(iq, session): for item in session['custom_data']: results.append(item) session = {'custom_data': ['foo'], 'error': handle_error} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.failUnless(results == ['foo'], 'Incomplete command workflow: %s' % results) def testClientAPIErrorStrippedResponse(self): """Test errors that don't include the command substanza.""" results = [] def handle_error(iq, session): for item in session['custom_data']: results.append(item) session = {'custom_data': ['foo'], 'error': handle_error} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.failUnless(results == ['foo'], 'Incomplete command workflow: %s' % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommands) slixmpp-1.2.2/tests/test_stream_roster.py0000644000175000001440000002206712467071246021660 0ustar mathieuiusers00000000000000# -*- encoding:utf-8 -*- from __future__ import unicode_literals import unittest from slixmpp.exceptions import IqTimeout from slixmpp.test import SlixTest import time import threading class TestStreamRoster(SlixTest): """ Test handling roster updates. """ def tearDown(self): self.stream_close() def testGetRoster(self): """Test handling roster requests.""" self.stream_start(mode='client', jid='tester@localhost') roster_updates = [] self.xmpp.add_event_handler('roster_update', roster_updates.append) self.xmpp.get_roster() self.send(""" """) self.recv(""" Friends Examples """) self.check_roster('tester@localhost', 'user@localhost', name='User', subscription='from', afrom=True, pending_out=True, groups=['Friends', 'Examples']) self.failUnless(len(roster_updates) == 1, "Wrong number of roster_update events fired: %s (should be 1)" % len(roster_updates)) def testRosterSet(self): """Test handling pushed roster updates.""" self.stream_start(mode='client') events = [] def roster_update(e): events.append('roster_update') self.xmpp.add_event_handler('roster_update', roster_update) self.recv(""" Friends Examples """) self.send(""" """) self.check_roster('tester@localhost', 'user@localhost', name='User', subscription='both', groups=['Friends', 'Examples']) self.failUnless('roster_update' in events, "Roster updated event not triggered: %s" % events) def testRosterPushRemove(self): """Test handling roster item removal updates.""" self.stream_start(mode='client') events = [] # Add roster item self.recv(""" Friends Examples """) self.send(""" """) self.assertTrue('user@localhost' in self.xmpp.client_roster) # Receive item remove push self.recv(""" """) self.send(""" """) self.assertTrue('user@localhost' not in self.xmpp.client_roster) def testUnauthorizedRosterPush(self): """Test rejecting a roster push from an unauthorized source.""" self.stream_start() self.recv(""" Friends Examples """) self.send(""" """) def testRosterCallback(self): """Test handling a roster request callback.""" self.stream_start() events = [] def roster_callback(iq): events.append('roster_callback') self.xmpp.get_roster(callback=roster_callback) self.send(""" """) self.recv(""" Friends Examples """) self.failUnless(events == ['roster_callback'], "Roster timeout event not triggered: %s." % events) def testRosterUnicode(self): """Test that JIDs with Unicode values are handled properly.""" self.stream_start(plugins=[]) self.recv(""" Unicode """) self.check_roster('tester@localhost', 'andré@foo', subscription='both', groups=['Unicode']) jids = list(self.xmpp.client_roster.keys()) self.failUnless(jids == ['andré@foo'], "Too many roster entries found: %s" % jids) self.recv(""" away Testing """) result = self.xmpp.client_roster['andré@foo'].resources expected = {'bar': {'status':'Testing', 'show':'away', 'priority':0}} self.failUnless(result == expected, "Unexpected roster values: %s" % result) def testSendLastPresence(self): """Test that sending the last presence works.""" self.stream_start(plugins=[]) self.xmpp.send_presence(pshow='dnd') self.xmpp.auto_authorize = True self.xmpp.auto_subscribe = True self.send(""" dnd """) self.recv(""" """) self.send(""" """) self.send(""" dnd """) def testUnsupportedRosterVer(self): """Test working with a server without roster versioning.""" self.stream_start() self.assertTrue('rosterver' not in self.xmpp.features) self.xmpp.get_roster() self.send(""" """) self.recv(""" """) def testBootstrapRosterVer(self): """Test bootstrapping with roster versioning.""" self.stream_start() self.xmpp.features.add('rosterver') self.xmpp.client_roster.version = '' self.xmpp.get_roster() self.send(""" """) self.recv(""" """) def testExistingRosterVer(self): """Test using a stored roster version.""" self.stream_start() self.xmpp.features.add('rosterver') self.xmpp.client_roster.version = '42' self.xmpp.get_roster() self.send(""" """) self.recv(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster) slixmpp-1.2.2/tests/test_tostring.py0000644000175000001440000000774712424504520020635 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest from slixmpp.xmlstream.stanzabase import ET from slixmpp.xmlstream.tostring import tostring, escape class TestToString(SlixTest): """ Test the implementation of slixmpp.xmlstream.tostring """ def tearDown(self): self.stream_close() def tryTostring(self, original='', expected=None, message='', **kwargs): """ Compare the result of calling tostring against an expected result. """ if not expected: expected=original if isinstance(original, str): xml = ET.fromstring(original) else: xml=original result = tostring(xml, **kwargs) self.failUnless(result == expected, "%s: %s" % (message, result)) def testXMLEscape(self): """Test escaping XML special characters.""" original = """'Hi & welcome!'""" escaped = escape(original) desired = """<foo bar="baz">'Hi""" desired += """ & welcome!'</foo>""" self.failUnless(escaped == desired, "XML escaping did not work: %s." % escaped) def testEmptyElement(self): """Test converting an empty element to a string.""" self.tryTostring( original='', message="Empty element not serialized correctly") def testEmptyElementWrapped(self): """Test converting an empty element inside another element.""" self.tryTostring( original='', message="Wrapped empty element not serialized correctly") def testEmptyElementWrappedText(self): """ Test converting an empty element wrapped with text inside another element. """ self.tryTostring( original='Some text. More text.', message="Text wrapped empty element serialized incorrectly") def testMultipleChildren(self): """Test converting multiple child elements to a Unicode string.""" self.tryTostring( original='', message="Multiple child elements not serialized correctly") def testXMLNS(self): """ Test using xmlns tostring parameter, which will prevent adding an xmlns attribute to the serialized element if the element's namespace is the same. """ self.tryTostring( original='', expected='', message="The xmlns parameter was not used properly.", xmlns='foo') def testTailContent(self): """ Test that elements of the form foo bar baz only include " baz" once. """ self.tryTostring( original='
foo bar baz', message='Element tail content is incorrect.') def testStanzaStr(self): """ Test that stanza objects are serialized properly. """ self.stream_start() utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0' if not hasattr(utf8_message, 'decode'): # Python 3 utf8_message = bytes(utf8_message, encoding='utf-8') msg = self.Message() msg['body'] = utf8_message.decode('utf-8') expected = '\xe0\xb2\xa0_\xe0\xb2\xa0' result = msg.__str__() self.failUnless(result == expected, "Stanza Unicode handling is incorrect: %s" % result) def testXMLLang(self): """Test that serializing xml:lang works.""" self.stream_start() msg = self.Message() msg._set_attr('{%s}lang' % msg.xml_ns, "no") expected = '' result = msg.__str__() self.failUnless(expected == result, "Serialization with xml:lang failed: %s" % result) suite = unittest.TestLoader().loadTestsFromTestCase(TestToString) slixmpp-1.2.2/tests/live_multiple_streams.py0000644000175000001440000000343112424504520022317 0ustar mathieuiusers00000000000000import logging from slixmpp.test import * class TestMultipleStreams(SlixTest): """ Test that we can test a live stanza stream. """ def setUp(self): self.client1 = SlixTest() self.client2 = SlixTest() def tearDown(self): self.client1.stream_close() self.client2.stream_close() def testMultipleStreams(self): """Test that we can interact with multiple live ClientXMPP instance.""" client1 = self.client1 client2 = self.client2 client1.stream_start(mode='client', socket='live', skip=True, jid='user@localhost/test1', password='user') client2.stream_start(mode='client', socket='live', skip=True, jid='user@localhost/test2', password='user') client1.xmpp.send_message(mto='user@localhost/test2', mbody='test') client1.send('message@body=test', method='stanzapath') client2.recv('message@body=test', method='stanzapath') suite = unittest.TestLoader().loadTestsFromTestCase(TestMultipleStreams) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') tests = unittest.TestSuite([suite]) result = unittest.TextTestRunner(verbosity=2).run(tests) test_ns = 'http://andyet.net/protocol/tests' print("" % ( test_ns, 'ran="%s"' % result.testsRun, 'errors="%s"' % len(result.errors), 'fails="%s"' % len(result.failures), 'success="%s"' % result.wasSuccessful())) slixmpp-1.2.2/tests/test_stanza_xep_0325.py0000644000175000001440000002061312603534742021604 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.test import * import slixmpp.plugins.xep_0325 as xep_0325 namespace='sn' class TestControlStanzas(SlixTest): def setUp(self): pass def testSetRequest(self): """ test of set request stanza """ iq = self.Iq() iq['type'] = 'set' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '1' iq['set'].add_node("Device02", "Source02", "MyCacheType") iq['set'].add_node("Device15") iq['set'].add_data("Tjohej", "boolean", "true") self.check(iq,""" """ ) iq['set'].del_node("Device02") self.check(iq,""" """ ) iq['set'].del_nodes() self.check(iq,""" """ ) def testDirectSet(self): """ test of direct set stanza """ msg = self.Message() msg['from'] = 'master@clayster.com/amr' msg['to'] = 'device@clayster.com' msg['set'].add_node("Device02") msg['set'].add_node("Device15") msg['set'].add_data("Tjohej", "boolean", "true") self.check(msg,""" """ ) def testSetResponse(self): """ test of set response stanza """ iq = self.Iq() iq['type'] = 'result' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '8' iq['setResponse']['responseCode'] = "OK" self.check(iq,""" """ ) iq = self.Iq() iq['type'] = 'error' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '9' iq['setResponse']['responseCode'] = "OtherError" iq['setResponse']['error']['var'] = "Output" iq['setResponse']['error']['text'] = "Test of other error.!" self.check(iq,""" Test of other error.! """ ) iq = self.Iq() iq['type'] = 'error' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '9' iq['setResponse']['responseCode'] = "NotFound" iq['setResponse'].add_node("Device17", "Source09") iq['setResponse'].add_node("Device18", "Source09") iq['setResponse'].add_data("Tjohopp") self.check(iq,""" """ ) def testSetRequestDatas(self): """ test of set request data stanzas """ iq = self.Iq() iq['type'] = 'set' iq['from'] = 'master@clayster.com/amr' iq['to'] = 'device@clayster.com' iq['id'] = '1' iq['set'].add_node("Device02", "Source02", "MyCacheType") iq['set'].add_node("Device15") iq['set'].add_data("Tjohej", "boolean", "true") iq['set'].add_data("Tjohej2", "boolean", "false") iq['set'].add_data("TjohejC", "color", "FF00FF") iq['set'].add_data("TjohejC2", "color", "00FF00") iq['set'].add_data("TjohejS", "string", "String1") iq['set'].add_data("TjohejS2", "string", "String2") iq['set'].add_data("TjohejDate", "date", "2012-01-01") iq['set'].add_data("TjohejDate2", "date", "1900-12-03") iq['set'].add_data("TjohejDateT4", "dateTime", "1900-12-03 12:30") iq['set'].add_data("TjohejDateT2", "dateTime", "1900-12-03 11:22") iq['set'].add_data("TjohejDouble2", "double", "200.22") iq['set'].add_data("TjohejDouble3", "double", "-12232131.3333") iq['set'].add_data("TjohejDur", "duration", "P5Y") iq['set'].add_data("TjohejDur2", "duration", "PT2M1S") iq['set'].add_data("TjohejInt", "int", "1") iq['set'].add_data("TjohejInt2", "int", "-42") iq['set'].add_data("TjohejLong", "long", "123456789098") iq['set'].add_data("TjohejLong2", "long", "-90983243827489374") iq['set'].add_data("TjohejTime", "time", "23:59") iq['set'].add_data("TjohejTime2", "time", "12:00") self.check(iq,""" """ ) suite = unittest.TestLoader().loadTestsFromTestCase(TestControlStanzas) slixmpp-1.2.2/tests/live_test.py0000644000175000001440000000653312424504520017713 0ustar mathieuiusers00000000000000import logging from slixmpp.test import * class TestLiveStream(SlixTest): """ Test that we can test a live stanza stream. """ def tearDown(self): self.stream_close() def testClientConnection(self): """Test that we can interact with a live ClientXMPP instance.""" self.stream_start(mode='client', socket='live', skip=False, jid='user@localhost/test', password='user') # Use sid=None to ignore any id sent by the server since # we can't know it in advance. self.recv_header(sfrom='localhost', sid=None) self.send_header(sto='localhost') self.recv_feature(""" DIGEST-MD5 PLAIN """) self.send_feature(""" """) self.recv_feature(""" """) self.send_header(sto='localhost') self.recv_header(sfrom='localhost', sid=None) self.recv_feature(""" DIGEST-MD5 PLAIN """) self.send_feature(""" AHVzZXIAdXNlcg== """) self.recv_feature(""" """) self.send_header(sto='localhost') self.recv_header(sfrom='localhost', sid=None) self.recv_feature(""" """) # Should really use send, but our Iq stanza objects # can't handle bind element payloads yet. self.send_feature(""" test """) self.recv_feature(""" user@localhost/test """) self.stream_close() suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') tests = unittest.TestSuite([suite]) result = unittest.TextTestRunner(verbosity=2).run(tests) test_ns = 'http://andyet.net/protocol/tests' print("" % ( test_ns, 'ran="%s"' % result.testsRun, 'errors="%s"' % len(result.errors), 'fails="%s"' % len(result.failures), 'success="%s"' % result.wasSuccessful())) slixmpp-1.2.2/tests/test_stream_xep_0030.py0000644000175000001440000004467613004224717021601 0ustar mathieuiusers00000000000000import time import threading import unittest from slixmpp.test import SlixTest class TestStreamDisco(SlixTest): """ Test using the XEP-0030 plugin. """ def tearDown(self): self.stream_close() def testInfoEmptyDefaultNode(self): """ Info query result from an entity MUST have at least one identity and feature, namely http://jabber.org/protocol/disco#info. Since the XEP-0030 plugin is loaded, a disco response should be generated and not an error result. """ self.stream_start(mode='client', plugins=['xep_0030']) self.recv(""" """) self.send(""" """) def testInfoEmptyDefaultNodeComponent(self): """ Test requesting an empty, default node using a Component. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) self.recv(""" """) self.send(""" """) def testInfoIncludeNode(self): """ Results for info queries directed to a particular node MUST include the node in the query response. """ self.stream_start(mode='client', plugins=['xep_0030']) self.xmpp['xep_0030'].static.add_node(node='testing') self.recv(""" """) self.send(""" """, method='mask') def testItemsIncludeNode(self): """ Results for items queries directed to a particular node MUST include the node in the query response. """ self.stream_start(mode='client', plugins=['xep_0030']) self.xmpp['xep_0030'].static.add_node(node='testing') self.recv(""" """) self.send(""" """, method='mask') def testDynamicInfoJID(self): """ Test using a dynamic info handler for a particular JID. """ self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', jid='tester@localhost', handler=dynamic_jid) self.recv(""" """) self.send(""" """) def testDynamicInfoGlobal(self): """ Test using a dynamic info handler for all requests. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', handler=dynamic_global) self.recv(""" """) self.send(""" """) def testOverrideJIDInfoHandler(self): """Test overriding a JID info handler.""" self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', jid='tester@localhost', handler=dynamic_jid) self.xmpp['xep_0030'].restore_defaults(jid='tester@localhost', node='testing') self.xmpp['xep_0030'].add_identity(jid='tester@localhost', node='testing', category='automation', itype='command-list') self.recv(""" """) self.send(""" """) def testOverrideGlobalInfoHandler(self): """Test overriding the global JID info handler.""" self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', handler=dynamic_global) self.xmpp['xep_0030'].restore_defaults(jid='user@tester.localhost', node='testing') self.xmpp['xep_0030'].add_feature(jid='user@tester.localhost', node='testing', feature='urn:xmpp:ping') self.recv(""" """) self.send(""" """) def testGetInfoRemote(self): """ Test sending a disco#info query to another entity and receiving the result. """ self.stream_start(mode='client', plugins=['xep_0030']) events = set() def handle_disco_info(iq): events.add('disco_info') self.xmpp.add_event_handler('disco_info', handle_disco_info) self.xmpp['xep_0030'].get_info('user@localhost', 'foo') self.send(""" """) self.recv(""" """) self.assertEqual(events, {'disco_info'}, "Disco info event was not triggered: %s" % events) def testDynamicItemsJID(self): """ Test using a dynamic items handler for a particular JID. """ self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='JID') return result self.xmpp['xep_0030'].set_node_handler('get_items', jid='tester@localhost', handler=dynamic_jid) self.recv(""" """) self.send(""" """) def testDynamicItemsGlobal(self): """ Test using a dynamic items handler for all requests. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', handler=dynamic_global) self.recv(""" """) self.send(""" """) def testOverrideJIDItemsHandler(self): """Test overriding a JID items handler.""" self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', jid='tester@localhost', handler=dynamic_jid) self.xmpp['xep_0030'].restore_defaults(jid='tester@localhost', node='testing') self.xmpp['xep_0030'].add_item(ijid='tester@localhost', node='testing', jid='tester@localhost', subnode='foo', name='Test') self.recv(""" """) self.send(""" """) def testOverrideGlobalItemsHandler(self): """Test overriding the global JID items handler.""" self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester.localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', handler=dynamic_global) self.xmpp['xep_0030'].restore_defaults(jid='user@tester.localhost', node='testing') self.xmpp['xep_0030'].add_item(ijid='user@tester.localhost', node='testing', jid='user@tester.localhost', subnode='foo', name='Test') self.recv(""" """) self.send(""" """) def testGetItemsRemote(self): """ Test sending a disco#items query to another entity and receiving the result. """ self.stream_start(mode='client', plugins=['xep_0030']) events = set() results = set() def handle_disco_items(iq): events.add('disco_items') results.update(iq['disco_items']['items']) self.xmpp.add_event_handler('disco_items', handle_disco_items) self.xmpp['xep_0030'].get_items('user@localhost', 'foo') self.send(""" """) self.recv(""" """) items = {('user@localhost', 'bar', 'Test'), ('user@localhost', 'baz', 'Test 2')} self.assertEqual(events, {'disco_items'}, "Disco items event was not triggered: %s" % events) self.assertEqual(results, items, "Unexpected items: %s" % results) ''' def testGetItemsIterator(self): """Test interaction between XEP-0030 and XEP-0059 plugins.""" raised_exceptions = [] self.stream_start(mode='client', plugins=['xep_0030', 'xep_0059']) results = self.xmpp['xep_0030'].get_items(jid='foo@localhost', node='bar', iterator=True) results.amount = 10 def run_test(): try: results.next() except StopIteration: raised_exceptions.append(True) t = threading.Thread(name="get_items_iterator", target=run_test) t.start() self.send(""" 10 """) self.recv(""" """) t.join() self.assertEqual(raised_exceptions, [True], "StopIteration was not raised: %s" % raised_exceptions) ''' suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDisco) slixmpp-1.2.2/tests/test_stream_xep_0060.py0000644000175000001440000006165312467104545021606 0ustar mathieuiusers00000000000000import threading import unittest from slixmpp.test import SlixTest from slixmpp.stanza.atom import AtomEntry from slixmpp.xmlstream import register_stanza_plugin class TestStreamPubsub(SlixTest): """ Test using the XEP-0030 plugin. """ def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testCreateInstantNode(self): """Test creating an instant node""" self.xmpp['xep_0060'].create_node('pubsub.example.com', None) self.send(""" """) self.recv(""" """) def testCreateNodeNoConfig(self): """Test creating a node without a config""" self.xmpp['xep_0060'].create_node( 'pubsub.example.com', 'princely_musings') self.send(""" """) def testCreateNodeConfig(self): """Test creating a node with a config""" form = self.xmpp['xep_0004'].stanza.Form() form['type'] = 'submit' form.add_field(var='pubsub#access_model', value='whitelist') self.xmpp['xep_0060'].create_node( 'pubsub.example.com', 'princely_musings', config=form) self.send(""" whitelist http://jabber.org/protocol/pubsub#node_config """) def testDeleteNode(self): """Test deleting a node""" self.xmpp['xep_0060'].delete_node( 'pubsub.example.com', 'some_node') self.send(""" """) def testSubscribeCase1(self): """ Test subscribing to a node: Case 1: No subscribee, default 'from' JID, bare JID """ self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode') self.send(""" """) def testSubscribeCase2(self): """ Test subscribing to a node: Case 2: No subscribee, given 'from' JID, bare JID """ self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode', ifrom='foo@comp.example.com/bar') self.send(""" """) def testSubscribeCase3(self): """ Test subscribing to a node: Case 3: No subscribee, given 'from' JID, full JID """ self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode', ifrom='foo@comp.example.com/bar', bare=False) self.send(""" """) def testSubscribeCase4(self): """ Test subscribing to a node: Case 4: No subscribee, no 'from' JID, full JID """ self.stream_close() self.stream_start(jid='tester@localhost/full') self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode', bare=False) self.send(""" """) def testSubscribeCase5(self): """ Test subscribing to a node: Case 5: Subscribee given """ self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode', subscribee='user@example.com/foo', ifrom='foo@comp.example.com/bar') self.send(""" """) def testSubscribeWithOptions(self): """Test subscribing to a node, with options.""" opts = self.xmpp['xep_0004'].make_form() opts.add_field( var='FORM_TYPE', value='http://jabber.org/protocol/pubsub#subscribe_options', ftype='hidden') opts.add_field( var='pubsub#digest', value=False, ftype='boolean') opts['type'] = 'submit' self.xmpp['xep_0060'].subscribe( 'pubsub.example.com', 'somenode', options=opts) self.send(""" http://jabber.org/protocol/pubsub#subscribe_options 0 """) def testUnsubscribeCase1(self): """ Test unsubscribing from a node: Case 1: No subscribee, default 'from' JID, bare JID """ self.xmpp['xep_0060'].unsubscribe( 'pubsub.example.com', 'somenode') self.send(""" """) def testUnsubscribeCase2(self): """ Test unsubscribing from a node: Case 2: No subscribee, given 'from' JID, bare JID """ self.xmpp['xep_0060'].unsubscribe( 'pubsub.example.com', 'somenode', ifrom='foo@comp.example.com/bar') self.send(""" """) def testUnsubscribeCase3(self): """ Test unsubscribing from a node: Case 3: No subscribee, given 'from' JID, full JID """ self.xmpp['xep_0060'].unsubscribe( 'pubsub.example.com', 'somenode', ifrom='foo@comp.example.com/bar', bare=False) self.send(""" """) def testUnsubscribeCase4(self): """ Test unsubscribing from a node: Case 4: No subscribee, no 'from' JID, full JID """ self.stream_close() self.stream_start(jid='tester@localhost/full') self.xmpp['xep_0060'].unsubscribe( 'pubsub.example.com', 'somenode', bare=False) self.send(""" """) def testUnsubscribeCase5(self): """ Test unsubscribing from a node: Case 5: Subscribee given """ self.xmpp['xep_0060'].unsubscribe( 'pubsub.example.com', 'somenode', subscribee='user@example.com/foo', ifrom='foo@comp.example.com/bar') self.send(""" """) def testGetDefaultNodeConfig(self): """Test retrieving the default node config for a pubsub service.""" self.xmpp['xep_0060'].get_node_config( 'pubsub.example.com') self.send(""" """, use_values=False) def testGetNodeConfig(self): """Test getting the config for a given node.""" self.xmpp['xep_0060'].get_node_config( 'pubsub.example.com', 'somenode') self.send(""" """, use_values=False) def testSetNodeConfig(self): """Test setting the configuration for a node.""" form = self.xmpp['xep_0004'].make_form() form.add_field(var='FORM_TYPE', ftype='hidden', value='http://jabber.org/protocol/pubsub#node_config') form.add_field(var='pubsub#title', ftype='text-single', value='This is awesome!') form['type'] = 'submit' self.xmpp['xep_0060'].set_node_config( 'pubsub.example.com', 'somenode', form) self.send(""" http://jabber.org/protocol/pubsub#node_config This is awesome! """) def testPublishNoItems(self): """Test publishing no items (in order to generate events)""" self.xmpp['xep_0060'].publish( 'pubsub.example.com', 'somenode') self.send(""" """) def testPublishSingle(self): """Test publishing a single item.""" payload = AtomEntry() payload['title'] = 'Test' register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, AtomEntry) self.xmpp['xep_0060'].publish( 'pubsub.example.com', 'somenode', id='id42', payload=payload) self.send(""" Test """, use_values=False) def testPublishSingleOptions(self): """Test publishing a single item, with options.""" payload = AtomEntry() payload['title'] = 'Test' register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, AtomEntry) options = self.xmpp['xep_0004'].make_form() options.add_field(var='FORM_TYPE', ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') options.add_field(var='pubsub#access_model', ftype='text-single', value='presence') options['type'] = 'submit' self.xmpp['xep_0060'].publish( 'pubsub.example.com', 'somenode', id='ID42', payload=payload, options=options) self.send(""" Test http://jabber.org/protocol/pubsub#publish-options presence """, use_values=False) def testRetract(self): """Test deleting an item.""" self.xmpp['xep_0060'].retract( 'pubsub.example.com', 'somenode', 'ID1', notify=True) self.send(""" """) def testRetract(self): """Test deleting an item.""" self.xmpp['xep_0060'].retract( 'pubsub.example.com', 'somenode', 'ID1') self.send(""" """) def testPurge(self): """Test removing all items from a node.""" self.xmpp['xep_0060'].purge( 'pubsub.example.com', 'somenode') self.send(""" """) def testGetItem(self): """Test retrieving a single item.""" self.xmpp['xep_0060'].get_item( 'pubsub.example.com', 'somenode', 'id42') self.send(""" """) def testGetLatestItems(self): """Test retrieving the most recent N items.""" self.xmpp['xep_0060'].get_items( 'pubsub.example.com', 'somenode', max_items=3) self.send(""" """) def testGetAllItems(self): """Test retrieving all items.""" self.xmpp['xep_0060'].get_items( 'pubsub.example.com', 'somenode') self.send(""" """) def testGetSpecificItems(self): """Test retrieving a specific set of items.""" self.xmpp['xep_0060'].get_items( 'pubsub.example.com', 'somenode', item_ids=['A', 'B', 'C']) self.send(""" """) def testGetSubscriptionGlobalDefaultOptions(self): """Test getting the subscription options for a node/JID.""" self.xmpp['xep_0060'].get_subscription_options( 'pubsub.example.com') self.send(""" """, use_values=False) def testGetSubscriptionNodeDefaultOptions(self): """Test getting the subscription options for a node/JID.""" self.xmpp['xep_0060'].get_subscription_options( 'pubsub.example.com', node='somenode') self.send(""" """, use_values=False) def testGetSubscriptionOptions(self): """Test getting the subscription options for a node/JID.""" self.xmpp['xep_0060'].get_subscription_options( 'pubsub.example.com', 'somenode', 'tester@localhost') self.send(""" """, use_values=False) def testSetSubscriptionOptions(self): """Test setting the subscription options for a node/JID.""" opts = self.xmpp['xep_0004'].make_form() opts.add_field( var='FORM_TYPE', value='http://jabber.org/protocol/pubsub#subscribe_options', ftype='hidden') opts.add_field( var='pubsub#digest', value=False, ftype='boolean') opts['type'] = 'submit' self.xmpp['xep_0060'].set_subscription_options( 'pubsub.example.com', 'somenode', 'tester@localhost', opts) self.send(""" http://jabber.org/protocol/pubsub#subscribe_options 0 """) def testGetNodeSubscriptions(self): """Test retrieving all subscriptions for a node.""" self.xmpp['xep_0060'].get_node_subscriptions( 'pubsub.example.com', 'somenode') self.send(""" """) def testGetSubscriptions(self): """Test retrieving a users's subscriptions.""" self.xmpp['xep_0060'].get_subscriptions( 'pubsub.example.com') self.send(""" """) def testGetSubscriptionsForNode(self): """Test retrieving a users's subscriptions for a given node.""" self.xmpp['xep_0060'].get_subscriptions( 'pubsub.example.com', node='somenode') self.send(""" """) def testGetAffiliations(self): """Test retrieving a users's affiliations.""" self.xmpp['xep_0060'].get_affiliations( 'pubsub.example.com') self.send(""" """) def testGetAffiliatinssForNode(self): """Test retrieving a users's affiliations for a given node.""" self.xmpp['xep_0060'].get_affiliations( 'pubsub.example.com', node='somenode') self.send(""" """) def testGetNodeAffiliations(self): """Test getting the affiliations for a node.""" self.xmpp['xep_0060'].get_node_affiliations( 'pubsub.example.com', 'somenode') self.send(""" """) def testModifySubscriptions(self): """Test owner modifying node subscriptions.""" self.xmpp['xep_0060'].modify_subscriptions( 'pubsub.example.com', 'somenode', subscriptions=[('user@example.com', 'subscribed'), ('foo@example.net', 'none')]) self.send(""" """) def testModifyAffiliations(self): """Test owner modifying node affiliations.""" self.xmpp['xep_0060'].modify_affiliations( 'pubsub.example.com', 'somenode', affiliations=[('user@example.com', 'publisher'), ('foo@example.net', 'none')]) self.send(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPubsub) slixmpp-1.2.2/tests/test_events.py0000644000175000001440000000506312467064104020263 0ustar mathieuiusers00000000000000import time import unittest from slixmpp.test import SlixTest class TestEvents(SlixTest): def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testEventHappening(self): """Test handler working""" happened = [] def handletestevent(event): happened.append(True) self.xmpp.add_event_handler("test_event", handletestevent) self.xmpp.event("test_event") self.xmpp.event("test_event") msg = "Event was not triggered the correct number of times: %s" self.failUnless(happened == [True, True], msg) def testDelEvent(self): """Test handler working, then deleted and not triggered""" 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", {}) 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", {}) 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 = [] def handletestevent(event): happened.append(True) self.xmpp.add_event_handler("test_event", handletestevent, disposable=True) self.xmpp.event("test_event", {}) # Should not trigger because it was deleted self.xmpp.event("test_event", {}) msg = "Event was not triggered the correct number of times: %s" self.failUnless(happened == [True], msg % happened) suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents) slixmpp-1.2.2/tests/test_stream_xep_0085.py0000644000175000001440000000324612467102365021605 0ustar mathieuiusers00000000000000import time import unittest from slixmpp.test import SlixTest class TestStreamChatStates(SlixTest): def tearDown(self): self.stream_close() def testChatStates(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0085']) results = [] def handle_state(msg): results.append(msg['chat_state']) self.xmpp.add_event_handler('chatstate_active', handle_state) self.xmpp.add_event_handler('chatstate_inactive', handle_state) self.xmpp.add_event_handler('chatstate_paused', handle_state) self.xmpp.add_event_handler('chatstate_gone', handle_state) self.xmpp.add_event_handler('chatstate_composing', handle_state) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) expected = ['active', 'inactive', 'paused', 'composing', 'gone'] self.failUnless(results == expected, "Chat state event not handled: %s" % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamChatStates) slixmpp-1.2.2/tests/test_stream_filters.py0000644000175000001440000000353312467064114022003 0ustar mathieuiusers00000000000000import time from slixmpp import Message import unittest from slixmpp.test import SlixTest class TestFilters(SlixTest): """ Test using incoming and outgoing filters. """ def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testIncoming(self): data = [] def in_filter(stanza): if isinstance(stanza, Message): if stanza['body'] == 'testing': stanza['subject'] = stanza['body'] + ' filter' print('>>> %s' % stanza['subject']) return stanza def on_message(msg): print('<<< %s' % msg['subject']) data.append(msg['subject']) self.xmpp.add_filter('in', in_filter) self.xmpp.add_event_handler('message', on_message) self.recv(""" no filter """) self.recv(""" testing """) self.assertEqual(data, ['', 'testing filter'], 'Incoming filter did not apply %s' % data) def testOutgoing(self): def out_filter(stanza): if isinstance(stanza, Message): if stanza['body'] == 'testing': stanza['body'] = 'changed!' return stanza self.xmpp.add_filter('out', out_filter) m1 = self.Message() m1['body'] = 'testing' m1.send() m2 = self.Message() m2['body'] = 'blah' m2.send() self.send(""" changed! """) self.send(""" blah """) suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters) slixmpp-1.2.2/tests/test_plugins.py0000644000175000001440000001014013004224717020424 0ustar mathieuiusers00000000000000import unittest import logging from slixmpp.plugins.base import PluginManager, BasePlugin, register_plugin class A(BasePlugin): name = 'a' class B(BasePlugin): name = 'b' class C(BasePlugin): name = 'c' dependencies = {'b', 'd'} class D(BasePlugin): name = 'd' dependencies = {'c'} class E(BasePlugin): name = 'e' dependencies = {'a', 'd'} class F(BasePlugin): name = 'f' dependencies = {'a', 'b'} register_plugin(A) register_plugin(B) register_plugin(C) register_plugin(D) register_plugin(E) register_plugin(F) class TestPlugins(unittest.TestCase): def test_enable(self): """Enable a single plugin.""" p = PluginManager(None) events = [] def init(self): events.append('init') A.plugin_init = init p.enable('a') self.assertEqual(len(p), 1, "Wrong number of enabled plugins.") self.assertEqual(events, ['init'], "Plugin init method not called.") def test_disable(self): """Disable a single plugin.""" p = PluginManager(None) events = [] def init(self): events.append('init') def end(self): events.append('end') A.plugin_init = init A.plugin_end = end p.enable('a') p.disable('a') self.assertEqual(len(p), 0, "Wrong number of enabled plugins.") self.assertEqual(events, ['init', 'end'], "Plugin lifecycle methods not called.") def test_enable_dependencies(self): """Enable a plugin with acyclic dependencies.""" p = PluginManager(None) events = [] A.plugin_init = lambda s: events.append('init_a') B.plugin_init = lambda s: events.append('init_b') p.enable('f') self.assertEqual(len(p), 3, "Wrong number of enabled plugins.") self.assertTrue('init_a' in events, "Dependency A not enabled.") self.assertTrue('init_b' in events, "Dependency B not enabled.") def test_enable_cyclic_dependencies(self): """Enable a plugin with cyclic dependencies.""" p = PluginManager(None) events = [] B.plugin_init = lambda s: events.append('init_b') C.plugin_init = lambda s: events.append('init_c') D.plugin_init = lambda s: events.append('init_d') p.enable('c') self.assertEqual(len(p), 3, "Wrong number of enabled plugins.") self.assertTrue('init_b' in events, "Dependency B not enabled.") self.assertTrue('init_c' in events, "Dependency C not enabled.") self.assertTrue('init_d' in events, "Dependency D not enabled.") def test_disable_dependendents(self): """Disable a plugin with dependents.""" p = PluginManager(None) events = [] A.plugin_end = lambda s: events.append('end_a') B.plugin_end = lambda s: events.append('end_b') F.plugin_end = lambda s: events.append('end_f') p.enable('f') p.disable('a') self.assertEqual(len(p), 1, "Wrong number of enabled plugins.") self.assertTrue('end_f' in events, "Dependent F not disabled.") self.assertTrue('end_a' in events, "Plugin A not disabled.") def test_disable_cyclic_dependents(self): """Disable a plugin with cyclic dependents.""" p = PluginManager(None) events = [] B.plugin_end = lambda s: events.append('end_b') C.plugin_end = lambda s: events.append('end_c') D.plugin_end = lambda s: events.append('end_d') p.enable('c') p.disable('b') self.assertEqual(len(p), 0, "Wrong number of enabled plugins.") self.assertTrue('end_b' in events, "Plugin B not disabled.") self.assertTrue('end_c' in events, "Dependent C not disabled.") self.assertTrue('end_d' in events, "Dependent D not disabled.") suite = unittest.TestLoader().loadTestsFromTestCase(TestPlugins) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') tests = unittest.TestSuite([suite]) unittest.TextTestRunner(verbosity=2).run(tests) slixmpp-1.2.2/tests/test_stream_xep_0066.py0000644000175000001440000000173012467104631021576 0ustar mathieuiusers00000000000000import threading import unittest from slixmpp.test import SlixTest class TestOOB(SlixTest): def tearDown(self): self.stream_close() def testSendOOB(self): """Test sending an OOB transfer request.""" self.stream_start(plugins=['xep_0066', 'xep_0030']) url = 'http://github.com/fritzy/Slixmpp/blob/master/README' self.xmpp['xep_0066'].send_oob('user@example.com', url, desc='Slixmpp README') self.send(""" http://github.com/fritzy/Slixmpp/blob/master/README Slixmpp README """) self.recv(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB) slixmpp-1.2.2/tests/test_stream_xep_0249.py0000644000175000001440000000312712467102404021577 0ustar mathieuiusers00000000000000import time import unittest from slixmpp.test import SlixTest class TestStreamDirectInvite(SlixTest): """ Test using the XEP-0249 plugin. """ def tearDown(self): self.stream_close() def testReceiveInvite(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0249']) events = [] def handle_invite(msg): events.append(True) self.xmpp.add_event_handler('groupchat_direct_invite', handle_invite) self.recv(""" """) self.failUnless(events == [True], "Event not raised: %s" % events) def testSentDirectInvite(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0249']) self.xmpp['xep_0249'].send_invitation('user@example.com', 'sleek@conference.jabber.org', reason='Need to test Slixmpp') self.send(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDirectInvite) slixmpp-1.2.2/tests/test_stanza_error.py0000644000175000001440000000472412466746741021510 0ustar mathieuiusers00000000000000import unittest from slixmpp.test import SlixTest class TestErrorStanzas(SlixTest): def setUp(self): # Ensure that the XEP-0086 plugin has been loaded. self.stream_start() self.stream_close() def testSetup(self): """Test setting initial values in error stanza.""" msg = self.Message() msg.enable('error') self.check(msg, """ """) def testCondition(self): """Test modifying the error condition.""" msg = self.Message() msg['error']['condition'] = 'item-not-found' self.check(msg, """ """) self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.") msg['error']['condition'] = 'resource-constraint' self.check(msg, """ """) def testDelCondition(self): """Test that deleting error conditions doesn't remove extra elements.""" msg = self.Message() msg['error']['text'] = 'Error!' msg['error']['condition'] = 'internal-server-error' del msg['error']['condition'] self.check(msg, """ Error! """, use_values=False) def testDelText(self): """Test deleting the text of an error.""" msg = self.Message() msg['error']['test'] = 'Error!' msg['error']['condition'] = 'internal-server-error' del msg['error']['text'] self.check(msg, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas) slixmpp-1.2.2/PKG-INFO0000644000175000001440000001471413014656513015304 0ustar mathieuiusers00000000000000Metadata-Version: 1.1 Name: slixmpp Version: 1.2.2 Summary: Slixmpp is an elegant Python library for XMPP (aka Jabber, Google Talk, etc). Home-page: https://dev.louiz.org/projects/slixmpp Author: Florent Le Coz Author-email: louiz@louiz.org License: MIT Description: Slixmpp ######### Slixmpp is an MIT licensed XMPP library for Python 3.4+. It is a fork of SleekXMPP. Slixmpp's goals is to only rewrite the core of the library (the low level socket handling, the timers, the events dispatching) in order to remove all threads. Building -------- Slixmpp can make use of cython to improve performance on critical modules. To do that, **cython3** is necessary along with **libidn** headers. Otherwise, no compilation is needed. Building is done by running setup.py:: python3 setup.py build_ext --inplace Documentation and Testing ------------------------- Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``. To generate the Sphinx documentation, follow the commands below. The HTML output will be in ``docs/_build/html``:: cd docs make html open _build/html/index.html To run the test suite for Slixmpp:: python run_tests.py The Slixmpp Boilerplate ------------------------- Projects using Slixmpp tend to follow a basic pattern for setting up client/component connections and configuration. Here is the gist of the boilerplate needed for a Slixmpp based project. See the documetation or examples directory for more detailed archetypes for Slixmpp projects:: import logging from slixmpp import ClientXMPP from slixmpp.exceptions import IqError, IqTimeout class EchoBot(ClientXMPP): def __init__(self, jid, password): ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) # If you wanted more functionality, here's how to register plugins: # self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0199') # XMPP Ping # Here's how to access plugins once you've registered them: # self['xep_0030'].add_feature('echo_demo') # 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() self.get_roster() # Most get_*/set_* methods from plugins use Iq stanzas, which # can generate IqError and IqTimeout exceptions # # try: # self.get_roster() # except IqError as err: # logging.error('There was an error getting the roster') # logging.error(err.iq['error']['condition']) # self.disconnect() # except IqTimeout: # logging.error('Server is taking too long to respond') # self.disconnect() def message(self, msg): if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') xmpp = EchoBot('somejid@example.com', 'use_getpass') xmpp.connect() xmpp.process(forever=True) Slixmpp Credits --------------- **Maintainers:** - Florent Le Coz (`louiz@louiz.org `_), - Mathieu Pasquet (`mathieui@mathieui.net `_), **Contributors:** - Emmanuel Gil Peyrot (`Link mauve `_) - Sam Whited (`Sam Whited `_) - Dan Sully (`Dan Sully `_) - Gasper Zejn (`Gasper Zejn `_) - Krzysztof Kotlenga (`Krzysztof Kotlenga `_) - Tsukasa Hiiragi (`Tsukasa Hiiragi `_) Credits (SleekXMPP) ------------------- **Main Author:** Nathan Fritz `fritzy@netflint.net `_, `@fritzy `_ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP `_, and a former member of the XMPP Council. **Co-Author:** Lance Stout `lancestout@gmail.com `_, `@lancestout `_ **Contributors:** - Brian Beggs (`macdiesel `_) - Dann Martens (`dannmartens `_) - Florent Le Coz (`louiz `_) - Kevin Smith (`Kev `_, http://kismith.co.uk) - Remko Tronçon (`remko `_, http://el-tramo.be) - Te-jé Rogers (`te-je `_) - Thom Nichols (`tomstrummer `_) Platform: any Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Libraries :: Python Modules slixmpp-1.2.2/slixmpp/0000755000175000001440000000000013014656513015674 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/0000755000175000001440000000000013014656513017512 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/__init__.py0000644000175000001440000000051212424504517021621 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ __all__ = [ 'feature_starttls', 'feature_mechanisms', 'feature_bind', 'feature_session', 'feature_rosterver', 'feature_preapproval' ] slixmpp-1.2.2/slixmpp/features/feature_bind/0000755000175000001440000000000013014656513022141 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_bind/__init__.py0000644000175000001440000000055612770302340024252 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_bind.bind import FeatureBind from slixmpp.features.feature_bind.stanza import Bind register_plugin(FeatureBind) slixmpp-1.2.2/slixmpp/features/feature_bind/bind.py0000644000175000001440000000374012722310521023422 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import asyncio import logging from slixmpp.jid import JID from slixmpp.stanza import Iq, StreamFeatures from slixmpp.features.feature_bind import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin log = logging.getLogger(__name__) class FeatureBind(BasePlugin): name = 'feature_bind' description = 'RFC 6120: Stream Feature: Resource Binding' dependencies = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('bind', self._handle_bind_resource, restart=False, order=10000) register_stanza_plugin(Iq, stanza.Bind) register_stanza_plugin(StreamFeatures, stanza.Bind) @asyncio.coroutine def _handle_bind_resource(self, features): """ Handle requesting a specific resource. Arguments: features -- The stream features stanza. """ log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource) self.features = features iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') if self.xmpp.requested_jid.resource: iq['bind']['resource'] = self.xmpp.requested_jid.resource yield from iq.send(callback=self._on_bind_response) def _on_bind_response(self, response): self.xmpp.boundjid = JID(response['bind']['jid']) self.xmpp.bound = True self.xmpp.event('session_bind', self.xmpp.boundjid) self.xmpp.session_bind_event.set() self.xmpp.features.add('bind') log.info("JID set to: %s", self.xmpp.boundjid.full) if 'session' not in self.features['features']: log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') slixmpp-1.2.2/slixmpp/features/feature_bind/stanza.py0000644000175000001440000000065013004224717024010 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Bind(ElementBase): """ """ name = 'bind' namespace = 'urn:ietf:params:xml:ns:xmpp-bind' interfaces = {'resource', 'jid'} sub_interfaces = interfaces plugin_attrib = 'bind' slixmpp-1.2.2/slixmpp/features/feature_mechanisms/0000755000175000001440000000000013014656513023354 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_mechanisms/__init__.py0000644000175000001440000000111412770302340025454 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms from slixmpp.features.feature_mechanisms.stanza import Mechanisms from slixmpp.features.feature_mechanisms.stanza import Auth from slixmpp.features.feature_mechanisms.stanza import Success from slixmpp.features.feature_mechanisms.stanza import Failure register_plugin(FeatureMechanisms) slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/0000755000175000001440000000000013014656513024654 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/failure.py0000644000175000001440000000452213004224717026654 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import StanzaBase, ET class Failure(StanzaBase): """ """ name = 'failure' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'condition', 'text'} plugin_attrib = name sub_interfaces = {'text'} conditions = {'aborted', 'account-disabled', 'credentials-expired', 'encryption-required', 'incorrect-encoding', 'invalid-authzid', 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak', 'not-authorized', 'temporary-auth-failure'} def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup. Sets a default error type and condition, and changes the parent stanza's type to 'error'. Arguments: xml -- Use an existing XML object for the stanza's values. """ # StanzaBase overrides self.namespace self.namespace = Failure.namespace if StanzaBase.setup(self, xml): #If we had to generate XML then set default values. self['condition'] = 'not-authorized' self.xml.tag = self.tag_name() def get_condition(self): """Return the condition element's name.""" for child in self.xml: if "{%s}" % self.namespace in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: return cond return 'not-authorized' def set_condition(self, value): """ Set the tag name of the condition element. Arguments: value -- The tag name of the condition element. """ if value in self.conditions: del self['condition'] self.xml.append(ET.Element("{%s}%s" % (self.namespace, value))) return self def del_condition(self): """Remove the condition element.""" for child in self.xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: self.xml.remove(child) return self slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/__init__.py0000644000175000001440000000124012424504517026762 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms from slixmpp.features.feature_mechanisms.stanza.auth import Auth from slixmpp.features.feature_mechanisms.stanza.success import Success from slixmpp.features.feature_mechanisms.stanza.failure import Failure from slixmpp.features.feature_mechanisms.stanza.challenge import Challenge from slixmpp.features.feature_mechanisms.stanza.response import Response from slixmpp.features.feature_mechanisms.stanza.abort import Abort slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/auth.py0000644000175000001440000000232513004224717026165 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Auth(StanzaBase): """ """ name = 'auth' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'mechanism', 'value'} plugin_attrib = name #: Some SASL mechs require sending values as is, #: without converting base64. plain_mechs = {'X-MESSENGER-OAUTH2'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): if not self['mechanism'] in self.plain_mechs: return base64.b64decode(bytes(self.xml.text)) else: return self.xml.text def set_value(self, values): if not self['mechanism'] in self.plain_mechs: if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') elif values == b'': self.xml.text = '=' else: self.xml.text = bytes(values).decode('utf-8') def del_value(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/abort.py0000644000175000001440000000073612424504517026343 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import StanzaBase class Abort(StanzaBase): """ """ name = 'abort' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = set() plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/challenge.py0000644000175000001440000000151413004224717027145 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Challenge(StanzaBase): """ """ name = 'challenge' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/response.py0000644000175000001440000000151213004224717027057 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Response(StanzaBase): """ """ name = 'response' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/mechanisms.py0000644000175000001440000000230213004224717027346 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class Mechanisms(ElementBase): """ """ name = 'mechanisms' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'mechanisms', 'required'} plugin_attrib = name is_extension = True def get_required(self): """ """ return True def get_mechanisms(self): """ """ results = [] mechs = self.xml.findall('{%s}mechanism' % self.namespace) if mechs: for mech in mechs: results.append(mech.text) return results def set_mechanisms(self, values): """ """ self.del_mechanisms() for val in values: mech = ET.Element('{%s}mechanism' % self.namespace) mech.text = val self.append(mech) def del_mechanisms(self): """ """ mechs = self.xml.findall('{%s}mechanism' % self.namespace) if mechs: for mech in mechs: self.xml.remove(mech) slixmpp-1.2.2/slixmpp/features/feature_mechanisms/stanza/success.py0000644000175000001440000000150713004224717026675 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Success(StanzaBase): """ """ name = 'success' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/features/feature_mechanisms/mechanisms.py0000644000175000001440000002163213004224717026055 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import ssl import logging from slixmpp.util import sasl from slixmpp.util.stringprep_profiles import StringPrepError from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.features.feature_mechanisms import stanza log = logging.getLogger(__name__) class FeatureMechanisms(BasePlugin): name = 'feature_mechanisms' description = 'RFC 6120: Stream Feature: SASL' dependencies = set() stanza = stanza default_config = { 'use_mech': None, 'use_mechs': None, 'min_mech': None, 'sasl_callback': None, 'security_callback': None, 'encrypted_plain': True, 'unencrypted_plain': False, 'unencrypted_digest': False, 'unencrypted_cram': False, 'unencrypted_scram': True, 'order': 100 } def plugin_init(self): if self.sasl_callback is None: self.sasl_callback = self._default_credentials if self.security_callback is None: self.security_callback = self._default_security creds = self.sasl_callback({'username'}, set()) if not self.use_mech and not creds['username']: self.use_mech = 'ANONYMOUS' self.mech = None self.mech_list = set() self.attempted_mechs = set() register_stanza_plugin(StreamFeatures, stanza.Mechanisms) self.xmpp.register_stanza(stanza.Success) self.xmpp.register_stanza(stanza.Failure) self.xmpp.register_stanza(stanza.Auth) self.xmpp.register_stanza(stanza.Challenge) self.xmpp.register_stanza(stanza.Response) self.xmpp.register_stanza(stanza.Abort) self.xmpp.register_handler( Callback('SASL Success', MatchXPath(stanza.Success.tag_name()), self._handle_success, instream=True)) self.xmpp.register_handler( Callback('SASL Failure', MatchXPath(stanza.Failure.tag_name()), self._handle_fail, instream=True)) self.xmpp.register_handler( Callback('SASL Challenge', MatchXPath(stanza.Challenge.tag_name()), self._handle_challenge)) self.xmpp.register_feature('mechanisms', self._handle_sasl_auth, restart=True, order=self.order) def _default_credentials(self, required_values, optional_values): creds = self.xmpp.credentials result = {} values = required_values.union(optional_values) for value in values: if value == 'username': result[value] = creds.get('username', self.xmpp.requested_jid.user) elif value == 'email': jid = self.xmpp.requested_jid.bare result[value] = creds.get('email', jid) elif value == 'channel_binding': if hasattr(self.xmpp.socket, 'get_channel_binding'): result[value] = self.xmpp.socket.get_channel_binding() else: log.debug("Channel binding not supported.") log.debug("Use Python 3.3+ for channel binding and " + \ "SCRAM-SHA-1-PLUS support") result[value] = None elif value == 'host': result[value] = creds.get('host', self.xmpp.requested_jid.domain) elif value == 'realm': result[value] = creds.get('realm', self.xmpp.requested_jid.domain) elif value == 'service-name': result[value] = creds.get('service-name', self.xmpp._service_name) elif value == 'service': result[value] = creds.get('service', 'xmpp') elif value in creds: result[value] = creds[value] return result def _default_security(self, values): result = {} for value in values: if value == 'encrypted': if 'starttls' in self.xmpp.features: result[value] = True elif isinstance(self.xmpp.socket, ssl.SSLSocket): result[value] = True else: result[value] = False else: result[value] = self.config.get(value, False) return result def _handle_sasl_auth(self, features): """ Handle authenticating using SASL. Arguments: features -- The stream features stanza. """ if 'mechanisms' in self.xmpp.features: # SASL authentication has already succeeded, but the # server has incorrectly offered it again. return False enforce_limit = False limited_mechs = self.use_mechs if limited_mechs is None: limited_mechs = set() elif limited_mechs and not isinstance(limited_mechs, set): limited_mechs = set(limited_mechs) enforce_limit = True if self.use_mech: limited_mechs.add(self.use_mech) enforce_limit = True if enforce_limit: self.use_mechs = limited_mechs self.mech_list = set(features['mechanisms']) return self._send_auth() def _send_auth(self): mech_list = self.mech_list - self.attempted_mechs try: self.mech = sasl.choose(mech_list, self.sasl_callback, self.security_callback, limit=self.use_mechs, min_mech=self.min_mech) except sasl.SASLNoAppropriateMechanism: log.error("No appropriate login method.") self.xmpp.event("failed_all_auth") if not self.attempted_mechs: # Only trigger this event if we didn't try at least one # method self.xmpp.event("no_auth") self.attempted_mechs = set() return self.xmpp.disconnect() except StringPrepError: log.exception("A credential value did not pass SASLprep.") self.xmpp.disconnect() resp = stanza.Auth(self.xmpp) resp['mechanism'] = self.mech.name try: resp['value'] = self.mech.process() except sasl.SASLCancelled: self.attempted_mechs.add(self.mech.name) self._send_auth() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() except sasl.SASLFailed: self.attempted_mechs.add(self.mech.name) self._send_auth() else: resp.send() return True def _handle_challenge(self, stanza): """SASL challenge received. Process and send response.""" resp = self.stanza.Response(self.xmpp) try: resp['value'] = self.mech.process(stanza['value']) except sasl.SASLCancelled: self.stanza.Abort(self.xmpp).send() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() except sasl.SASLFailed: self.stanza.Abort(self.xmpp).send() else: if resp.get_value() == '': resp.del_value() resp.send() def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" try: final = self.mech.process(stanza['value']) except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() else: self.attempted_mechs = set() self.xmpp.authenticated = True self.xmpp.features.add('mechanisms') self.xmpp.event('auth_success', stanza) # Restart the stream self.xmpp.init_parser() self.xmpp.send_raw(self.xmpp.stream_header) def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" self.attempted_mechs.add(self.mech.name) log.info("Authentication failed: %s", stanza['condition']) self.xmpp.event("failed_auth", stanza) self._send_auth() return True slixmpp-1.2.2/slixmpp/features/feature_preapproval/0000755000175000001440000000000013014656513023560 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_preapproval/preapproval.py0000644000175000001440000000222412424504517026465 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import StreamFeatures from slixmpp.features.feature_preapproval import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin log = logging.getLogger(__name__) class FeaturePreApproval(BasePlugin): name = 'feature_preapproval' description = 'RFC 6121: Stream Feature: Subscription Pre-Approval' dependences = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('preapproval', self._handle_preapproval, restart=False, order=9001) register_stanza_plugin(StreamFeatures, stanza.PreApproval) def _handle_preapproval(self, features): """Save notice that the server support subscription pre-approvals. Arguments: features -- The stream features stanza. """ log.debug("Server supports subscription pre-approvals.") self.xmpp.features.add('preapproval') slixmpp-1.2.2/slixmpp/features/feature_preapproval/__init__.py0000644000175000001440000000063012424504517025670 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_preapproval.preapproval import FeaturePreApproval from slixmpp.features.feature_preapproval.stanza import PreApproval register_plugin(FeaturePreApproval) slixmpp-1.2.2/slixmpp/features/feature_preapproval/stanza.py0000644000175000001440000000056412424504517025437 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class PreApproval(ElementBase): name = 'sub' namespace = 'urn:xmpp:features:pre-approval' interfaces = set() plugin_attrib = 'preapproval' slixmpp-1.2.2/slixmpp/features/feature_rosterver/0000755000175000001440000000000013014656513023260 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_rosterver/__init__.py0000644000175000001440000000061412770302340025364 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_rosterver.rosterver import FeatureRosterVer from slixmpp.features.feature_rosterver.stanza import RosterVer register_plugin(FeatureRosterVer) slixmpp-1.2.2/slixmpp/features/feature_rosterver/rosterver.py0000644000175000001440000000211412424504517025663 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import StreamFeatures from slixmpp.features.feature_rosterver import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin log = logging.getLogger(__name__) class FeatureRosterVer(BasePlugin): name = 'feature_rosterver' description = 'RFC 6121: Stream Feature: Roster Versioning' dependences = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('rosterver', self._handle_rosterver, restart=False, order=9000) register_stanza_plugin(StreamFeatures, stanza.RosterVer) def _handle_rosterver(self, features): """Enable using roster versioning. Arguments: features -- The stream features stanza. """ log.debug("Enabling roster versioning.") self.xmpp.features.add('rosterver') slixmpp-1.2.2/slixmpp/features/feature_rosterver/stanza.py0000644000175000001440000000055512424504517025137 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class RosterVer(ElementBase): name = 'ver' namespace = 'urn:xmpp:features:rosterver' interfaces = set() plugin_attrib = 'rosterver' slixmpp-1.2.2/slixmpp/features/feature_session/0000755000175000001440000000000013014656513022710 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_session/__init__.py0000644000175000001440000000060012770302340025007 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_session.session import FeatureSession from slixmpp.features.feature_session.stanza import Session register_plugin(FeatureSession) slixmpp-1.2.2/slixmpp/features/feature_session/session.py0000644000175000001440000000270112722310255024740 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import asyncio import logging from slixmpp.stanza import Iq, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.features.feature_session import stanza log = logging.getLogger(__name__) class FeatureSession(BasePlugin): name = 'feature_session' description = 'RFC 3920: Stream Feature: Start Session' dependencies = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('session', self._handle_start_session, restart=False, order=10001) register_stanza_plugin(Iq, stanza.Session) register_stanza_plugin(StreamFeatures, stanza.Session) @asyncio.coroutine def _handle_start_session(self, features): """ Handle the start of the session. Arguments: feature -- The stream features element. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('session') yield from iq.send(callback=self._on_start_session_response) def _on_start_session_response(self, response): self.xmpp.features.add('session') log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') slixmpp-1.2.2/slixmpp/features/feature_session/stanza.py0000644000175000001440000000060612424504517024564 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Session(ElementBase): """ """ name = 'session' namespace = 'urn:ietf:params:xml:ns:xmpp-session' interfaces = set() plugin_attrib = 'session' slixmpp-1.2.2/slixmpp/features/feature_starttls/0000755000175000001440000000000013014656513023105 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/features/feature_starttls/__init__.py0000644000175000001440000000057712770302340025221 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_starttls.starttls import FeatureSTARTTLS from slixmpp.features.feature_starttls.stanza import * register_plugin(FeatureSTARTTLS) slixmpp-1.2.2/slixmpp/features/feature_starttls/starttls.py0000644000175000001440000000375612617172401025347 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.features.feature_starttls import stanza log = logging.getLogger(__name__) class FeatureSTARTTLS(BasePlugin): name = 'feature_starttls' description = 'RFC 6120: Stream Feature: STARTTLS' dependencies = set() stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('STARTTLS Proceed', MatchXPath(stanza.Proceed.tag_name()), self._handle_starttls_proceed, instream=True)) self.xmpp.register_feature('starttls', self._handle_starttls, restart=True, order=self.config.get('order', 0)) self.xmpp.register_stanza(stanza.Proceed) self.xmpp.register_stanza(stanza.Failure) register_stanza_plugin(StreamFeatures, stanza.STARTTLS) def _handle_starttls(self, features): """ Handle notification that the server supports TLS. Arguments: features -- The stream:features element. """ if 'starttls' in self.xmpp.features: # We have already negotiated TLS, but the server is # offering it again, against spec. return False elif self.xmpp.disable_starttls: return False else: self.xmpp.send(features['starttls']) return True def _handle_starttls_proceed(self, proceed): """Restart the XML stream when TLS is accepted.""" log.debug("Starting TLS") if self.xmpp.start_tls(): self.xmpp.features.add('starttls') slixmpp-1.2.2/slixmpp/features/feature_starttls/stanza.py0000644000175000001440000000136513004224717024760 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import StanzaBase, ElementBase class STARTTLS(ElementBase): """ """ name = 'starttls' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces = {'required'} plugin_attrib = name def get_required(self): """ """ return True class Proceed(StanzaBase): """ """ name = 'proceed' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces = set() class Failure(StanzaBase): """ """ name = 'failure' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces = set() slixmpp-1.2.2/slixmpp/jid.py0000644000175000001440000003207612774764672017046 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.jid ~~~~~~~~~~~~~~~~~~~~~~~ This module allows for working with Jabber IDs (JIDs). Part of Slixmpp: The Slick XMPP Library :copyright: (c) 2011 Nathanael C. Fritz :license: MIT, see LICENSE for more details """ import re import socket from copy import deepcopy from functools import lru_cache from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError HAVE_INET_PTON = hasattr(socket, 'inet_pton') #: The basic regex pattern that a JID must match in order to determine #: the local, domain, and resource parts. This regex does NOT do any #: validation, which requires application of nodeprep, resourceprep, etc. JID_PATTERN = re.compile( "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$" ) #: The set of escape sequences for the characters not allowed by nodeprep. JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f', '\\3a', '\\3c', '\\3e', '\\40', '\\5c'} #: The reverse mapping of escape sequences to their original forms. JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ', '\\22': '"', '\\26': '&', '\\27': "'", '\\2f': '/', '\\3a': ':', '\\3c': '<', '\\3e': '>', '\\40': '@', '\\5c': '\\'} # TODO: Find the best cache size for a standard usage. @lru_cache(maxsize=1024) def _parse_jid(data): """ Parse string data into the node, domain, and resource components of a JID, if possible. :param string data: A string that is potentially a JID. :raises InvalidJID: :returns: tuple of the validated local, domain, and resource strings """ match = JID_PATTERN.match(data) if not match: raise InvalidJID('JID could not be parsed') (node, domain, resource) = match.groups() node = _validate_node(node) domain = _validate_domain(domain) resource = _validate_resource(resource) return node, domain, resource def _validate_node(node): """Validate the local, or username, portion of a JID. :raises InvalidJID: :returns: The local portion of a JID, as validated by nodeprep. """ if node is None: return '' try: node = nodeprep(node) except StringprepError: raise InvalidJID('Nodeprep failed') if not node: raise InvalidJID('Localpart must not be 0 bytes') if len(node) > 1023: raise InvalidJID('Localpart must be less than 1024 bytes') return node def _validate_domain(domain): """Validate the domain portion of a JID. IP literal addresses are left as-is, if valid. Domain names are stripped of any trailing label separators (`.`), and are checked with the nameprep profile of stringprep. If the given domain is actually a punyencoded version of a domain name, it is converted back into its original Unicode form. Domains must also not start or end with a dash (`-`). :raises InvalidJID: :returns: The validated domain name """ ip_addr = False # First, check if this is an IPv4 address try: socket.inet_aton(domain) ip_addr = True except socket.error: pass # Check if this is an IPv6 address if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']': try: ip = domain[1:-1] socket.inet_pton(socket.AF_INET6, ip) ip_addr = True except (socket.error, ValueError): pass if not ip_addr: # This is a domain name, which must be checked further if domain and domain[-1] == '.': domain = domain[:-1] try: domain = idna(domain) except StringprepError: raise InvalidJID('idna validation failed') if ':' in domain: raise InvalidJID('Domain containing a port') for label in domain.split('.'): if not label: raise InvalidJID('Domain containing too many dots') if '-' in (label[0], label[-1]): raise InvalidJID('Domain started or ended with -') if not domain: raise InvalidJID('Domain must not be 0 bytes') if len(domain) > 1023: raise InvalidJID('Domain must be less than 1024 bytes') return domain def _validate_resource(resource): """Validate the resource portion of a JID. :raises InvalidJID: :returns: The local portion of a JID, as validated by resourceprep. """ if resource is None: return '' try: resource = resourceprep(resource) except StringprepError: raise InvalidJID('Resourceprep failed') if not resource: raise InvalidJID('Resource must not be 0 bytes') if len(resource) > 1023: raise InvalidJID('Resource must be less than 1024 bytes') return resource def _unescape_node(node): """Unescape a local portion of a JID. .. note:: The unescaped local portion is meant ONLY for presentation, and should not be used for other purposes. """ unescaped = [] seq = '' for i, char in enumerate(node): if char == '\\': seq = node[i:i+3] if seq not in JID_ESCAPE_SEQUENCES: seq = '' if seq: if len(seq) == 3: unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char)) # Pop character off the escape sequence, and ignore it seq = seq[1:] else: unescaped.append(char) return ''.join(unescaped) def _format_jid(local=None, domain=None, resource=None): """Format the given JID components into a full or bare JID. :param string local: Optional. The local portion of the JID. :param string domain: Required. The domain name portion of the JID. :param strin resource: Optional. The resource portion of the JID. :return: A full or bare JID string. """ if domain is None: return '' if local is not None: result = local + '@' + domain else: result = domain if resource is not None: result += '/' + resource return result class InvalidJID(ValueError): """ Raised when attempting to create a JID that does not pass validation. It can also be raised if modifying an existing JID in such a way as to make it invalid, such trying to remove the domain from an existing full JID while the local and resource portions still exist. """ # pylint: disable=R0903 class UnescapedJID: """ .. versionadded:: 1.1.10 """ __slots__ = ('_node', '_domain', '_resource') def __init__(self, node, domain, resource): self._node = node self._domain = domain self._resource = resource def __getattribute__(self, name): """Retrieve the given JID component. :param name: one of: user, server, domain, resource, full, or bare. """ if name == 'resource': return self._resource or '' if name in ('user', 'username', 'local', 'node'): return self._node or '' if name in ('server', 'domain', 'host'): return self._domain or '' if name in ('full', 'jid'): return _format_jid(self._node, self._domain, self._resource) if name == 'bare': return _format_jid(self._node, self._domain) return object.__getattribute__(self, name) def __str__(self): """Use the full JID as the string value.""" return _format_jid(self._node, self._domain, self._resource) def __repr__(self): """Use the full JID as the representation.""" return _format_jid(self._node, self._domain, self._resource) class JID: """ A representation of a Jabber ID, or JID. Each JID may have three components: a user, a domain, and an optional resource. For example: user@domain/resource When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. **JID Properties:** :full: The string value of the full JID. :jid: Alias for ``full``. :bare: The string value of the bare JID. :node: The node portion of the JID. :user: Alias for ``node``. :local: Alias for ``node``. :username: Alias for ``node``. :domain: The domain name portion of the JID. :server: Alias for ``domain``. :host: Alias for ``domain``. :resource: The resource portion of the JID. :param string jid: A string of the form ``'[user@]domain[/resource]'``. :raises InvalidJID: """ __slots__ = ('_node', '_domain', '_resource', '_bare', '_full') def __init__(self, jid=None): if not jid: self._node = '' self._domain = '' self._resource = '' self._bare = '' self._full = '' return elif not isinstance(jid, JID): self._node, self._domain, self._resource = _parse_jid(jid) else: self._node = jid._node self._domain = jid._domain self._resource = jid._resource self._update_bare_full() def unescape(self): """Return an unescaped JID object. Using an unescaped JID is preferred for displaying JIDs to humans, and they should NOT be used for any other purposes than for presentation. :return: :class:`UnescapedJID` .. versionadded:: 1.1.10 """ return UnescapedJID(_unescape_node(self._node), self._domain, self._resource) def _update_bare_full(self): """Format the given JID into a bare and a full JID. """ self._bare = (self._node + '@' + self._domain if self._node else self._domain) self._full = (self._bare + '/' + self._resource if self._resource else self._bare) @property def node(self): return self._node @property def user(self): return self._node @property def local(self): return self._node @property def username(self): return self._node @property def domain(self): return self._domain @property def server(self): return self._domain @property def host(self): return self._domain @property def resource(self): return self._resource @property def bare(self): return self._bare @property def full(self): return self._full @property def jid(self): return self._full @node.setter def node(self, value): self._node = _validate_node(value) self._update_bare_full() @user.setter def user(self, value): self._node = _validate_node(value) self._update_bare_full() @local.setter def local(self, value): self._node = _validate_node(value) self._update_bare_full() @username.setter def username(self, value): self._node = _validate_node(value) self._update_bare_full() @domain.setter def domain(self, value): self._domain = _validate_domain(value) self._update_bare_full() @server.setter def server(self, value): self._domain = _validate_domain(value) self._update_bare_full() @host.setter def host(self, value): self._domain = _validate_domain(value) self._update_bare_full() @bare.setter def bare(self, value): node, domain, resource = _parse_jid(value) assert not resource self._node = node self._domain = domain self._update_bare_full() @resource.setter def resource(self, value): self._resource = _validate_resource(value) self._update_bare_full() @full.setter def full(self, value): self._node, self._domain, self._resource = _parse_jid(value) self._update_bare_full() @jid.setter def jid(self, value): self._node, self._domain, self._resource = _parse_jid(value) self._update_bare_full() def __str__(self): """Use the full JID as the string value.""" return self._full def __repr__(self): """Use the full JID as the representation.""" return self._full # pylint: disable=W0212 def __eq__(self, other): """Two JIDs are equal if they have the same full JID value.""" if isinstance(other, UnescapedJID): return False if not isinstance(other, JID): other = JID(other) return (self._node == other._node and self._domain == other._domain and self._resource == other._resource) def __ne__(self, other): """Two JIDs are considered unequal if they are not equal.""" return not self == other def __hash__(self): """Hash a JID based on the string version of its full JID.""" return hash(self._full) slixmpp-1.2.2/slixmpp/roster/0000755000175000001440000000000013014656513017212 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/roster/single.py0000644000175000001440000002665612756434421021070 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import threading from slixmpp.xmlstream import JID from slixmpp.roster import RosterItem class RosterNode(object): """ A roster node is a roster for a single JID. Attributes: xmpp -- The main Slixmpp instance. jid -- The JID that owns the roster node. db -- Optional interface to an external datastore. auto_authorize -- Determines how authorizations are handled: True -- Accept all subscriptions. False -- Reject all subscriptions. None -- Subscriptions must be manually authorized. Defaults to True. auto_subscribe -- Determines if bi-directional subscriptions are created after automatically authrorizing a subscription request. Defaults to True last_status -- The last sent presence status that was broadcast to all contact JIDs. Methods: add -- Add a JID to the roster. update -- Update a JID's subscription information. subscribe -- Subscribe to a JID. unsubscribe -- Unsubscribe from a JID. remove -- Remove a JID from the roster. presence -- Return presence information for a JID's resources. send_presence -- Shortcut for sending a presence stanza. """ def __init__(self, xmpp, jid, db=None): """ Create a roster node for a JID. Arguments: xmpp -- The main Slixmpp instance. jid -- The JID that owns the roster. db -- Optional interface to an external datastore. """ self.xmpp = xmpp self.jid = jid self.db = db self.ignore_updates = False self.auto_authorize = True self.auto_subscribe = True self.last_status = None self._version = '' self._jids = {} self._last_status_lock = threading.Lock() if self.db: if hasattr(self.db, 'version'): self._version = self.db.version(self.jid) for jid in self.db.entries(self.jid): self.add(jid) @property def version(self): """Retrieve the roster's version ID.""" if self.db and hasattr(self.db, 'version'): self._version = self.db.version(self.jid) return self._version @version.setter def version(self, version): """Set the roster's version ID.""" self._version = version if self.db and hasattr(self.db, 'set_version'): self.db.set_version(self.jid, version) def __getitem__(self, key): """ Return the roster item for a subscribed JID. A new item entry will be created if one does not already exist. """ if key is None: key = JID('') if not isinstance(key, JID): key = JID(key) key = key.bare if key not in self._jids: self.add(key, save=True) return self._jids[key] def __delitem__(self, key): """ Remove a roster item from the local storage. To remove an item from the server, use the remove() method. """ if key is None: key = JID('') if not isinstance(key, JID): key = JID(key) key = key.bare if key in self._jids: del 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() def has_jid(self, jid): """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: groups = self._jids[jid]['groups'] if not groups: if '' not in result: result[''] = [] result[''].append(jid) for group in 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__() def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster node. Arguments: db -- The new datastore interface. save -- If True, save the existing state to the new backend datastore. Defaults to True. """ self.db = db existing_entries = set(self._jids) new_entries = set(self.db.entries(self.jid, {})) for jid in existing_entries: self._jids[jid].set_backend(db, save) for jid in new_entries - existing_entries: self.add(jid) def add(self, jid, name='', groups=None, afrom=False, ato=False, pending_in=False, pending_out=False, whitelisted=False, save=False): """ Add a new roster item entry. Arguments: jid -- The JID for the roster item. name -- An alias for the JID. groups -- A list of group names. afrom -- Indicates if the JID has a subscription state of 'from'. Defaults to False. ato -- Indicates if the JID has a subscription state of 'to'. Defaults to False. pending_in -- Indicates if the JID has sent a subscription request to this connection's JID. Defaults to False. pending_out -- Indicates if a subscription request has been sent to this JID. Defaults to False. whitelisted -- Indicates if a subscription request from this JID should be automatically authorized. Defaults to False. save -- Indicates if the item should be persisted immediately to an external datastore, if one is used. Defaults to False. """ if isinstance(jid, JID): key = jid.bare else: key = jid state = {'name': name, 'groups': groups or [], 'from': afrom, 'to': ato, 'pending_in': pending_in, 'pending_out': pending_out, 'whitelisted': whitelisted, 'subscription': 'none'} self._jids[key] = RosterItem(self.xmpp, jid, self.jid, state=state, db=self.db, roster=self) if save: self._jids[key].save() def subscribe(self, jid): """ Subscribe to the given JID. Arguments: jid -- The JID to subscribe to. """ self[jid].subscribe() def unsubscribe(self, jid): """ Unsubscribe from the given JID. Arguments: jid -- The JID to unsubscribe from. """ self[jid].unsubscribe() def remove(self, jid): """ Remove a JID from the roster. Arguments: jid -- The JID to remove. """ self[jid].remove() if not self.xmpp.is_component: return self.update(jid, subscription='remove') def update(self, jid, name=None, subscription=None, groups=[], timeout=None, callback=None, timeout_callback=None): """ Update a JID's subscription information. Arguments: jid -- The JID to update. name -- Optional alias for the JID. subscription -- The subscription state. May be one of: 'to', 'from', 'both', 'none', or 'remove'. groups -- A list of group names. timeout -- The length of time (in seconds) to wait for a response before continuing if blocking is used. Defaults to self.response_timeout. callback -- Optional reference to a stream handler function. Will be executed when the roster is received. """ if not groups: groups = [] self[jid]['name'] = name self[jid]['groups'] = groups self[jid].save() if not self.xmpp.is_component: iq = self.xmpp.Iq() iq['type'] = 'set' iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def presence(self, jid, resource=None): """ Retrieve the presence information of a JID. May return either all online resources' status, or a single resource's status. Arguments: jid -- The JID to lookup. resource -- Optional resource for returning only the status of a single connection. """ if resource is None: return self[jid].resources default_presence = {'status': '', 'priority': 0, 'show': ''} return self[jid].resources.get(resource, default_presence) def reset(self): """ Reset the state of the roster to forget any current presence information. Useful after a disconnection occurs. """ for jid in self: self[jid].reset() def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. If no recipient is specified, send the presence immediately. Otherwise, forward the send request to the recipient's roster entry for processing. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. pfrom -- The sender of a directed presence, which should be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. pnick -- Optional nickname of the presence's sender. """ if self.xmpp.is_component and not kwargs.get('pfrom', ''): kwargs['pfrom'] = self.jid self.xmpp.send_presence(**kwargs) def send_last_presence(self): if self.last_status is None: self.send_presence() else: pres = self.last_status if self.xmpp.is_component: pres['from'] = self.jid else: del pres['from'] pres.send() def __repr__(self): return repr(self._jids) slixmpp-1.2.2/slixmpp/roster/item.py0000644000175000001440000004773112441413611020527 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ class RosterItem(object): """ A RosterItem is a single entry in a roster node, and tracks the subscription state and user annotations of a single JID. Roster items may use an external datastore to persist roster data across sessions. Client applications will not need to use this functionality, but is intended for components that do not have their roster persisted automatically by the XMPP server. Roster items provide many methods for handling incoming presence stanzas that ensure that response stanzas are sent according to RFC 3921. The external datastore is accessed through a provided interface object which is stored in self.db. The interface object MUST provide two methods: load and save, both of which are responsible for working with a single roster item. A private dictionary, self._db_state, is used to store any metadata needed by the interface, such as the row ID of a roster item, etc. Interface for self.db.load: load(owner_jid, jid, db_state): owner_jid -- The JID that owns the roster. jid -- The JID of the roster item. db_state -- A dictionary containing any data saved by the interface object after a save() call. Will typically have the equivalent of a 'row_id' value. Interface for self.db.save: save(owner_jid, jid, item_state, db_state): owner_jid -- The JID that owns the roster. jid -- The JID of the roster item. item_state -- A dictionary containing the fields: 'from', 'to', 'pending_in', 'pending_out', 'whitelisted', 'subscription', 'name', and 'groups'. db_state -- A dictionary provided for persisting datastore specific information. Typically, a value equivalent to 'row_id' will be stored here. State Fields: from -- Indicates if a subscription of type 'from' has been authorized. to -- Indicates if a subscription of type 'to' has been authorized. pending_in -- Indicates if a subscription request has been received from this JID and it has not been authorized yet. pending_out -- Indicates if a subscription request has been sent to this JID and it has not been accepted yet. subscription -- Returns one of: 'to', 'from', 'both', or 'none' based on the states of from, to, pending_in, and pending_out. Assignment to this value does not affect the states of the other values. whitelisted -- Indicates if a subscription request from this JID should be automatically accepted. name -- A user supplied alias for the JID. groups -- A list of group names for the JID. Attributes: xmpp -- The main Slixmpp instance. owner -- The JID that owns the roster. jid -- The JID for the roster item. db -- Optional datastore interface object. last_status -- The last presence sent to this JID. resources -- A dictionary of online resources for this JID. Will contain the fields 'show', 'status', and 'priority'. Methods: load -- Retrieve the roster item from an external datastore, if one was provided. save -- Save the roster item to an external datastore, if one was provided. remove -- Remove a subscription to the JID and revoke its whitelisted status. subscribe -- Subscribe to the JID. authorize -- Accept a subscription from the JID. unauthorize -- Deny a subscription from the JID. unsubscribe -- Unsubscribe from the JID. send_presence -- Send a directed presence to the JID. send_last_presence -- Resend the last sent presence. handle_available -- Update the JID's resource information. handle_unavailable -- Update the JID's resource information. handle_subscribe -- Handle a subscription request. handle_subscribed -- Handle a notice that a subscription request was authorized by the JID. handle_unsubscribe -- Handle an unsubscribe request. handle_unsubscribed -- Handle a notice that a subscription was removed by the JID. handle_probe -- Handle a presence probe query. """ def __init__(self, xmpp, jid, owner=None, state=None, db=None, roster=None): """ Create a new roster item. Arguments: xmpp -- The main Slixmpp instance. jid -- The item's JID. owner -- The roster owner's JID. Defaults so self.xmpp.boundjid.bare. state -- A dictionary of initial state values. db -- An optional interface to an external datastore. roster -- The roster object containing this entry. """ self.xmpp = xmpp self.jid = jid self.owner = owner or self.xmpp.boundjid.bare self.last_status = None self.resources = {} self.roster = roster self.db = db self._state = state or { 'from': False, 'to': False, 'pending_in': False, 'pending_out': False, 'whitelisted': False, 'subscription': 'none', 'name': '', 'groups': []} self._db_state = {} self.load() def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster item. Arguments: db -- The new datastore interface. save -- If True, save the existing state to the new backend datastore. Defaults to True. """ self.db = db if save: self.save() self.load() def load(self): """ Load the item's state information from an external datastore, if one has been provided. """ if self.db: item = self.db.load(self.owner, self.jid, self._db_state) if item: self['name'] = item['name'] self['groups'] = item['groups'] self['from'] = item['from'] self['to'] = item['to'] self['whitelisted'] = item['whitelisted'] self['pending_out'] = item['pending_out'] self['pending_in'] = item['pending_in'] self['subscription'] = self._subscription() return self._state return None def save(self, remove=False): """ Save the item's state information to an external datastore, if one has been provided. Arguments: remove -- If True, expunge the item from the datastore. """ self['subscription'] = self._subscription() if remove: self._state['removed'] = True if self.db: self.db.save(self.owner, self.jid, self._state, self._db_state) # Finally, remove the in-memory copy if needed. if remove: del self.xmpp.roster[self.owner][self.jid] def __getitem__(self, key): """Return a state field's value.""" if key in self._state: if key == 'subscription': return self._subscription() return self._state[key] else: raise KeyError def __setitem__(self, key, value): """ Set the value of a state field. For boolean states, the values True, 'true', '1', 'on', and 'yes' are accepted as True; all others are False. Arguments: key -- The state field to modify. value -- The new value of the state field. """ if key in self._state: if key in ['name', 'subscription', 'groups']: self._state[key] = value else: value = str(value).lower() self._state[key] = value in ('true', '1', 'on', 'yes') else: raise KeyError def _subscription(self): """Return the proper subscription type based on current state.""" if self['to'] and self['from']: return 'both' elif self['from']: return 'from' elif self['to']: return 'to' else: return 'none' def remove(self): """ Remove a JID's whitelisted status and unsubscribe if a subscription exists. """ if self['to']: p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'unsubscribe' if self.xmpp.is_component: p['from'] = self.owner p.send() self['to'] = False self['whitelisted'] = False self.save() def subscribe(self): """Send a subscription request to the JID.""" p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'subscribe' if self.xmpp.is_component: p['from'] = self.owner self['pending_out'] = True self.save() p.send() def authorize(self): """Authorize a received subscription request from the JID.""" self['from'] = True self['pending_in'] = False self.save() self._subscribed() self.send_last_presence() def unauthorize(self): """Deny a received subscription request from the JID.""" self['from'] = False self['pending_in'] = False self.save() self._unsubscribed() p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'unavailable' if self.xmpp.is_component: p['from'] = self.owner p.send() def _subscribed(self): """Handle acknowledging a subscription.""" p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'subscribed' if self.xmpp.is_component: p['from'] = self.owner p.send() def unsubscribe(self): """Unsubscribe from the JID.""" p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'unsubscribe' if self.xmpp.is_component: p['from'] = self.owner self.save() p.send() def _unsubscribed(self): """Handle acknowledging an unsubscribe request.""" p = self.xmpp.Presence() p['to'] = self.jid p['type'] = 'unsubscribed' if self.xmpp.is_component: p['from'] = self.owner p.send() def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. If no recipient is specified, send the presence immediately. Otherwise, forward the send request to the recipient's roster entry for processing. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. pfrom -- The sender of a directed presence, which should be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. pnick -- Optional nickname of the presence's sender. """ if self.xmpp.is_component and not kwargs.get('pfrom', ''): kwargs['pfrom'] = self.owner if not kwargs.get('pto', ''): kwargs['pto'] = self.jid self.xmpp.send_presence(**kwargs) def send_last_presence(self): if self.last_status is None: pres = self.roster.last_status if pres is None: self.send_presence() else: pres['to'] = self.jid if self.xmpp.is_component: pres['from'] = self.owner else: del pres['from'] pres.send() else: self.last_status.send() def handle_available(self, presence): resource = presence['from'].resource data = {'status': presence['status'], 'show': presence['show'], 'priority': presence['priority']} got_online = not self.resources 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 got_online: self.xmpp.event('got_online', presence) 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 if not self.resources: 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) def handle_subscribe(self, presence): """ +------------------------------------------------------------------+ | EXISTING STATE | DELIVER? | NEW STATE | +------------------------------------------------------------------+ | "None" | yes | "None + Pending In" | | "None + Pending Out" | yes | "None + Pending Out/In" | | "None + Pending In" | no | no state change | | "None + Pending Out/In" | no | no state change | | "To" | yes | "To + Pending In" | | "To + Pending In" | no | no state change | | "From" | no * | no state change | | "From + Pending Out" | no * | no state change | | "Both" | no * | no state change | +------------------------------------------------------------------+ """ if self.xmpp.is_component: if not self['from'] and not self['pending_in']: self['pending_in'] = True self.xmpp.event('roster_subscription_request', presence) elif self['from']: self._subscribed() self.save() else: #server shouldn't send an invalid subscription request self.xmpp.event('roster_subscription_request', presence) def handle_subscribed(self, presence): """ +------------------------------------------------------------------+ | EXISTING STATE | DELIVER? | NEW STATE | +------------------------------------------------------------------+ | "None" | no | no state change | | "None + Pending Out" | yes | "To" | | "None + Pending In" | no | no state change | | "None + Pending Out/In" | yes | "To + Pending In" | | "To" | no | no state change | | "To + Pending In" | no | no state change | | "From" | no | no state change | | "From + Pending Out" | yes | "Both" | | "Both" | no | no state change | +------------------------------------------------------------------+ """ if self.xmpp.is_component: if not self['to'] and self['pending_out']: self['pending_out'] = False self['to'] = True self.xmpp.event('roster_subscription_authorized', presence) self.save() else: self.xmpp.event('roster_subscription_authorized', presence) def handle_unsubscribe(self, presence): """ +------------------------------------------------------------------+ | EXISTING STATE | DELIVER? | NEW STATE | +------------------------------------------------------------------+ | "None" | no | no state change | | "None + Pending Out" | no | no state change | | "None + Pending In" | yes * | "None" | | "None + Pending Out/In" | yes * | "None + Pending Out" | | "To" | no | no state change | | "To + Pending In" | yes * | "To" | | "From" | yes * | "None" | | "From + Pending Out" | yes * | "None + Pending Out | | "Both" | yes * | "To" | +------------------------------------------------------------------+ """ if self.xmpp.is_component: if not self['from'] and self['pending_in']: self['pending_in'] = False self._unsubscribed() elif self['from']: self['from'] = False self._unsubscribed() self.xmpp.event('roster_subscription_remove', presence) self.save() else: self.xmpp.event('roster_subscription_remove', presence) def handle_unsubscribed(self, presence): """ +------------------------------------------------------------------+ | EXISTING STATE | DELIVER? | NEW STATE | +------------------------------------------------------------------+ | "None" | no | no state change | | "None + Pending Out" | yes | "None" | | "None + Pending In" | no | no state change | | "None + Pending Out/In" | yes | "None + Pending In" | | "To" | yes | "None" | | "To + Pending In" | yes | "None + Pending In" | | "From" | no | no state change | | "From + Pending Out" | yes | "From" | | "Both" | yes | "From" | +------------------------------------------------------------------ """ if self.xmpp.is_component: if not self['to'] and self['pending_out']: self['pending_out'] = False elif self['to'] and not self['pending_out']: self['to'] = False self.xmpp.event('roster_subscription_removed', presence) self.save() else: self.xmpp.event('roster_subscription_removed', presence) def handle_probe(self, presence): if self['from']: self.send_last_presence() if self['pending_out']: self.subscribe() if not self['from']: self._unsubscribed() def reset(self): """ Forgot current resource presence information as part of a roster reset request. """ self.resources = {} def __repr__(self): return repr(self._state) slixmpp-1.2.2/slixmpp/roster/__init__.py0000644000175000001440000000045412424504520021320 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.roster.item import RosterItem from slixmpp.roster.single import RosterNode from slixmpp.roster.multi import Roster slixmpp-1.2.2/slixmpp/roster/multi.py0000644000175000001440000001571312424504520020717 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Presence from slixmpp.xmlstream import JID from slixmpp.roster import RosterNode class Roster(object): """ Slixmpp's roster manager. The roster is divided into "nodes", where each node is responsible for a single JID. While the distinction is not strictly necessary for client connections, it is a necessity for components that use multiple JIDs. Rosters may be stored and persisted in an external datastore. An interface object to the datastore that loads and saves roster items may be provided. See the documentation for the RosterItem class for the methods that the datastore interface object must provide. Attributes: xmpp -- The main Slixmpp instance. db -- Optional interface object to an external datastore. auto_authorize -- Default auto_authorize value for new roster nodes. Defaults to True. auto_subscribe -- Default auto_subscribe value for new roster nodes. Defaults to True. Methods: add -- Create a new roster node for a JID. send_presence -- Shortcut for sending a presence stanza. """ def __init__(self, xmpp, db=None): """ Create a new roster. Arguments: xmpp -- The main Slixmpp instance. db -- Optional interface object to a datastore. """ self.xmpp = xmpp self.db = db self._auto_authorize = True self._auto_subscribe = True self._rosters = {} if self.db: for node in self.db.entries(None, {}): self.add(node) self.xmpp.add_filter('out', self._save_last_status) def _save_last_status(self, stanza): if isinstance(stanza, Presence): sfrom = stanza['from'].full sto = stanza['to'].full if not sfrom: sfrom = self.xmpp.boundjid if stanza['type'] in stanza.showtypes or \ stanza['type'] in ('available', 'unavailable'): if sto: self[sfrom][sto].last_status = stanza else: self[sfrom].last_status = stanza with self[sfrom]._last_status_lock: for jid in self[sfrom]: self[sfrom][jid].last_status = None if not self.xmpp.sentpresence: self.xmpp.event('sent_presence') self.xmpp.sentpresence = True return stanza def __getitem__(self, key): """ Return the roster node for a JID. A new roster node will be created if one does not already exist. Arguments: key -- Return the roster for this JID. """ if key is None: key = self.xmpp.boundjid if not isinstance(key, JID): key = JID(key) key = key.bare if key not in self._rosters: self.add(key) self._rosters[key].auto_authorize = self.auto_authorize self._rosters[key].auto_subscribe = self.auto_subscribe return self._rosters[key] def keys(self): """Return the JIDs managed by the roster.""" return self._rosters.keys() def __iter__(self): """Iterate over the roster nodes.""" return self._rosters.__iter__() def add(self, node): """ Add a new roster node for the given JID. Arguments: node -- The JID for the new roster node. """ if not isinstance(node, JID): node = JID(node) node = node.bare if node not in self._rosters: self._rosters[node] = RosterNode(self.xmpp, node, self.db) def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster. Arguments: db -- The new datastore interface. save -- If True, save the existing state to the new backend datastore. Defaults to True. """ self.db = db existing_entries = set(self._rosters) new_entries = set(self.db.entries(None, {})) for node in existing_entries: self._rosters[node].set_backend(db, save) for node in new_entries - existing_entries: self.add(node) def reset(self): """ Reset the state of the roster to forget any current presence information. Useful after a disconnection occurs. """ for node in self: self[node].reset() def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. If no recipient is specified, send the presence immediately. Otherwise, forward the send request to the recipient's roster entry for processing. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. pfrom -- The sender of a directed presence, which should be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. pnick -- Optional nickname of the presence's sender. """ if self.xmpp.is_component and not kwargs.get('pfrom', ''): kwargs['pfrom'] = self.jid self.xmpp.send_presence(**kwargs) @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 def __repr__(self): return repr(self._rosters) slixmpp-1.2.2/slixmpp/stringprep.py0000644000175000001440000000671212572154127020453 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.stringprep ~~~~~~~~~~~~~~~~~~~~~~~ This module is a fallback using python’s stringprep instead of libidn’s. Part of Slixmpp: The Slick XMPP Library :copyright: (c) 2015 Emmanuel Gil Peyrot :license: MIT, see LICENSE for more details """ import logging import stringprep from slixmpp.util import stringprep_profiles import encodings.idna class StringprepError(Exception): pass #: These characters are not allowed to appear in a domain part. ILLEGAL_CHARS = ('\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' '\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' '\x1a\x1b\x1c\x1d\x1e\x1f' ' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f') # pylint: disable=c0103 #: The nodeprep profile of stringprep used to validate the local, #: or username, portion of a JID. _nodeprep = stringprep_profiles.create( nfkc=True, bidi=True, mappings=[ stringprep_profiles.b1_mapping, stringprep.map_table_b2], prohibited=[ stringprep.in_table_c11, stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9, lambda c: c in ' \'"&/:<>@'], unassigned=[stringprep.in_table_a1]) def nodeprep(node): try: return _nodeprep(node) except stringprep_profiles.StringPrepError: raise StringprepError # pylint: disable=c0103 #: The resourceprep profile of stringprep, which is used to validate #: the resource portion of a JID. _resourceprep = stringprep_profiles.create( nfkc=True, bidi=True, mappings=[stringprep_profiles.b1_mapping], prohibited=[ stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9], unassigned=[stringprep.in_table_a1]) def resourceprep(resource): try: return _resourceprep(resource) except stringprep_profiles.StringPrepError: raise StringprepError def idna(domain): domain_parts = [] for label in domain.split('.'): try: label = encodings.idna.nameprep(label) encodings.idna.ToASCII(label) except UnicodeError: raise StringprepError if label.startswith('xn--'): label = encodings.idna.ToUnicode(label) for char in label: if char in ILLEGAL_CHARS: raise StringprepError domain_parts.append(label) return '.'.join(domain_parts) def punycode(domain): domain_parts = [] for label in domain.split('.'): try: label = encodings.idna.nameprep(label) encodings.idna.ToASCII(label) except UnicodeError: raise StringprepError for char in label: if char in ILLEGAL_CHARS: raise StringprepError domain_parts.append(label) return b'.'.join(domain_parts) logging.getLogger(__name__).warning('Using slower stringprep, consider ' 'compiling the faster cython/libidn one.') slixmpp-1.2.2/slixmpp/__init__.py0000644000175000001440000000166412776257625020033 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import asyncio if hasattr(asyncio, 'sslproto'): # no ssl proto: very old asyncio = no need for this asyncio.sslproto._is_sslproto_available=lambda: False import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) from slixmpp.stanza import Message, Presence, Iq from slixmpp.jid import JID, InvalidJID from slixmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin from slixmpp.xmlstream.handler import * from slixmpp.xmlstream import XMLStream from slixmpp.xmlstream.matcher import * from slixmpp.xmlstream.asyncio import asyncio, future_wrapper from slixmpp.basexmpp import BaseXMPP from slixmpp.clientxmpp import ClientXMPP from slixmpp.componentxmpp import ComponentXMPP from slixmpp.version import __version__, __version_info__ slixmpp-1.2.2/slixmpp/util/0000755000175000001440000000000013014656513016651 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/util/__init__.py0000644000175000001440000000062512424504520020757 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.util ~~~~~~~~~~~~~~ Part of Slixmpp: The Slick XMPP Library :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout :license: MIT, see LICENSE for more details """ from slixmpp.util.misc_ops import bytes, unicode, hashes, hash, \ num_to_bytes, bytes_to_num, quote, \ XOR slixmpp-1.2.2/slixmpp/util/misc_ops.py0000644000175000001440000000723212755677207021061 0ustar mathieuiusers00000000000000import builtins import sys import hashlib def unicode(text): if not isinstance(text, str): return text.decode('utf-8') else: return text def bytes(text): """ Convert Unicode text to UTF-8 encoded bytes. Since Python 2.6+ and Python 3+ have similar but incompatible signatures, this function unifies the two to keep code sane. :param text: Unicode text to convert to bytes :rtype: bytes (Python3), str (Python2.6+) """ if text is None: return b'' if isinstance(text, builtins.bytes): # We already have bytes, so do nothing return text if isinstance(text, list): # Convert a list of integers to bytes return builtins.bytes(text) else: # Convert UTF-8 text to bytes return builtins.bytes(text, encoding='utf-8') def quote(text): """ Enclose in quotes and escape internal slashes and double quotes. :param text: A Unicode or byte string. """ text = bytes(text) return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"' def num_to_bytes(num): """ Convert an integer into a four byte sequence. :param integer num: An integer to convert to its byte representation. """ bval = b'' bval += bytes(chr(0xFF & (num >> 24))) bval += bytes(chr(0xFF & (num >> 16))) bval += bytes(chr(0xFF & (num >> 8))) bval += bytes(chr(0xFF & (num >> 0))) return bval def bytes_to_num(bval): """ Convert a four byte sequence to an integer. :param bytes bval: A four byte sequence to turn into an integer. """ num = 0 num += ord(bval[0] << 24) num += ord(bval[1] << 16) num += ord(bval[2] << 8) num += ord(bval[3]) return num def XOR(x, y): """ Return the results of an XOR operation on two equal length byte strings. :param bytes x: A byte string :param bytes y: A byte string :rtype: bytes """ # This operation is faster with a list comprehension than with a # generator, as of 2016 on python 3.5. return builtins.bytes([a ^ b for a, b in zip(x, y)]) def hash(name): """ Return a hash function implementing the given algorithm. :param name: The name of the hashing algorithm to use. :type name: string :rtype: function """ name = name.lower() if name.startswith('sha-'): name = 'sha' + name[4:] if name in dir(hashlib): return getattr(hashlib, name) return None def hashes(): """ Return a list of available hashing algorithms. :rtype: list of strings """ t = [] if 'md5' in dir(hashlib): t = ['MD5'] if 'md2' in dir(hashlib): t += ['MD2'] hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')] return t + hashes def setdefaultencoding(encoding): """ Set the current default string encoding used by the Unicode implementation. Actually calls sys.setdefaultencoding under the hood - see the docs for that for more details. This method exists only as a way to call find/call it even after it has been 'deleted' when the site module is executed. :param string encoding: An encoding name, compatible with sys.setdefaultencoding """ func = getattr(sys, 'setdefaultencoding', None) if func is None: import gc import types for obj in gc.get_objects(): if (isinstance(obj, types.BuiltinFunctionType) and obj.__name__ == 'setdefaultencoding'): func = obj break if func is None: raise RuntimeError("Could not find setdefaultencoding") sys.setdefaultencoding = func return func(encoding) slixmpp-1.2.2/slixmpp/util/stringprep_profiles.py0000644000175000001440000001035613010437511023317 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.util.stringprep_profiles ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module makes it easier to define profiles of stringprep, such as nodeprep and resourceprep for JID validation, and SASLprep for SASL. Part of Slixmpp: The Slick XMPP Library :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout :license: MIT, see LICENSE for more details """ from __future__ import unicode_literals import stringprep from unicodedata import ucd_3_2_0 as unicodedata from slixmpp.util import unicode class StringPrepError(UnicodeError): pass def b1_mapping(char): """Map characters that are commonly mapped to nothing.""" return '' if stringprep.in_table_b1(char) else None def c12_mapping(char): """Map non-ASCII whitespace to spaces.""" return ' ' if stringprep.in_table_c12(char) else None def map_input(data, tables=None): """ Each character in the input stream MUST be checked against a mapping table. """ result = [] for char in data: replacement = None for mapping in tables: replacement = mapping(char) if replacement is not None: break if replacement is None: replacement = char result.append(replacement) return ''.join(result) def normalize(data, nfkc=True): """ A profile can specify one of two options for Unicode normalization: - no normalization - Unicode normalization with form KC """ if nfkc: data = unicodedata.normalize('NFKC', data) return data def prohibit_output(data, tables=None): """ Before the text can be emitted, it MUST be checked for prohibited code points. """ for char in data: for check in tables: if check(char): raise StringPrepError("Prohibited code point: %s" % char) def check_bidi(data): """ 1) The characters in section 5.8 MUST be prohibited. 2) If a string contains any RandALCat character, the string MUST NOT contain any LCat character. 3) If a string contains any RandALCat character, a RandALCat character MUST be the first character of the string, and a RandALCat character MUST be the last character of the string. """ if not data: return data has_lcat = False has_randal = False for c in data: if stringprep.in_table_c8(c): raise StringPrepError("BIDI violation: seciton 6 (1)") if stringprep.in_table_d1(c): has_randal = True elif stringprep.in_table_d2(c): has_lcat = True if has_randal and has_lcat: raise StringPrepError("BIDI violation: section 6 (2)") first_randal = stringprep.in_table_d1(data[0]) last_randal = stringprep.in_table_d1(data[-1]) if has_randal and not (first_randal and last_randal): raise StringPrepError("BIDI violation: section 6 (3)") def create(nfkc=True, bidi=True, mappings=None, prohibited=None, unassigned=None): """Create a profile of stringprep. :param bool nfkc: If `True`, perform NFKC Unicode normalization. Defaults to `True`. :param bool bidi: If `True`, perform bidirectional text checks. Defaults to `True`. :param list mappings: Optional list of functions for mapping characters to suitable replacements. :param list prohibited: Optional list of functions which check for the presence of prohibited characters. :param list unassigned: Optional list of functions for detecting the use of unassigned code points. :raises: StringPrepError :return: Unicode string of the resulting text passing the profile's requirements. """ def profile(data, query=False): try: data = unicode(data) except UnicodeError: raise StringPrepError data = map_input(data, mappings) data = normalize(data, nfkc) prohibit_output(data, prohibited) if bidi: check_bidi(data) if query and unassigned: #check_unassigned(data, unassigned) raise StringPrepError('Query profile with unassigned data is unimplemented.') return data return profile slixmpp-1.2.2/slixmpp/util/sasl/0000755000175000001440000000000013014656513017613 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/util/sasl/client.py0000644000175000001440000001157513010437511021443 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.util.sasl.client ~~~~~~~~~~~~~~~~~~~~~~~~~~ This module was originally based on Dave Cridland's Suelta library. Part of Slixmpp: The Slick XMPP Library :copryight: (c) 2004-2013 David Alan Cridland :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout :license: MIT, see LICENSE for more details """ import logging import stringprep from slixmpp.util import hashes, bytes, stringprep_profiles log = logging.getLogger(__name__) #: Global registry mapping mechanism names to implementation classes. MECHANISMS = {} #: Global registry mapping mechanism names to security scores. MECH_SEC_SCORES = {} #: The SASLprep profile of stringprep used to validate simple username #: and password credentials. saslprep = stringprep_profiles.create( nfkc=True, bidi=True, mappings=[ stringprep_profiles.b1_mapping, stringprep_profiles.c12_mapping], prohibited=[ stringprep.in_table_c12, stringprep.in_table_c21, stringprep.in_table_c22, stringprep.in_table_c3, stringprep.in_table_c4, stringprep.in_table_c5, stringprep.in_table_c6, stringprep.in_table_c7, stringprep.in_table_c8, stringprep.in_table_c9], unassigned=[stringprep.in_table_a1]) def sasl_mech(score): sec_score = score def register(mech): n = 0 mech.score = sec_score if mech.use_hashes: for hashing_alg in hashes(): n += 1 score = mech.score + n name = '%s-%s' % (mech.name, hashing_alg) MECHANISMS[name] = mech MECH_SEC_SCORES[name] = score if mech.channel_binding: name += '-PLUS' score += 10 MECHANISMS[name] = mech MECH_SEC_SCORES[name] = score else: MECHANISMS[mech.name] = mech MECH_SEC_SCORES[mech.name] = mech.score if mech.channel_binding: MECHANISMS[mech.name + '-PLUS'] = mech MECH_SEC_SCORES[mech.name] = mech.score + 10 return mech return register class SASLNoAppropriateMechanism(Exception): def __init__(self, value=''): self.message = value class SASLCancelled(Exception): def __init__(self, value=''): self.message = value class SASLFailed(Exception): def __init__(self, value=''): self.message = value class SASLMutualAuthFailed(SASLFailed): def __init__(self, value=''): self.message = value class Mech(object): name = 'GENERIC' score = -1 use_hashes = False channel_binding = False required_credentials = set() optional_credentials = set() security = set() def __init__(self, name, credentials, security_settings): self.credentials = credentials self.security_settings = security_settings self.values = {} self.base_name = self.name self.name = name self.setup(name) def setup(self, name): pass def process(self, challenge=b''): return b'' def choose(mech_list, credentials, security_settings, limit=None, min_mech=None): available_mechs = set(MECHANISMS.keys()) if limit is None: limit = set(mech_list) if not isinstance(limit, set): limit = set(limit) if not isinstance(mech_list, set): mech_list = set(mech_list) mech_list = mech_list.intersection(limit) available_mechs = available_mechs.intersection(mech_list) best_score = MECH_SEC_SCORES.get(min_mech, -1) best_mech = None for name in available_mechs: if name in MECH_SEC_SCORES: if MECH_SEC_SCORES[name] > best_score: best_score = MECH_SEC_SCORES[name] best_mech = name if best_mech is None: raise SASLNoAppropriateMechanism() mech_class = MECHANISMS[best_mech] try: creds = credentials(mech_class.required_credentials, mech_class.optional_credentials) for req in mech_class.required_credentials: if req not in creds: raise SASLCancelled('Missing credential: %s' % req) for opt in mech_class.optional_credentials: if opt not in creds: creds[opt] = b'' for cred in creds: if cred in ('username', 'password', 'authzid'): creds[cred] = bytes(saslprep(creds[cred])) else: creds[cred] = bytes(creds[cred]) security_opts = security_settings(mech_class.security) return mech_class(best_mech, creds, security_opts) except SASLCancelled as e: log.info('SASL: %s: %s', best_mech, e.message) mech_list.remove(best_mech) return choose(mech_list, credentials, security_settings, limit=limit, min_mech=min_mech) slixmpp-1.2.2/slixmpp/util/sasl/__init__.py0000644000175000001440000000067112424504520021722 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.util.sasl ~~~~~~~~~~~~~~~~~~~ This module was originally based on Dave Cridland's Suelta library. Part of Slixmpp: The Slick XMPP Library :copryight: (c) 2004-2013 David Alan Cridland :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout :license: MIT, see LICENSE for more details """ from slixmpp.util.sasl.client import * from slixmpp.util.sasl.mechanisms import * slixmpp-1.2.2/slixmpp/util/sasl/mechanisms.py0000644000175000001440000004074013004224717022315 0ustar mathieuiusers00000000000000# -*- coding: utf-8 -*- """ slixmpp.util.sasl.mechanisms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A collection of supported SASL mechanisms. This module was originally based on Dave Cridland's Suelta library. Part of Slixmpp: The Slick XMPP Library :copryight: (c) 2004-2013 David Alan Cridland :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout :license: MIT, see LICENSE for more details """ import hmac import random from base64 import b64encode, b64decode from slixmpp.util import bytes, hash, XOR, quote, num_to_bytes from slixmpp.util.sasl.client import sasl_mech, Mech, \ SASLCancelled, SASLFailed, \ SASLMutualAuthFailed @sasl_mech(0) class ANONYMOUS(Mech): name = 'ANONYMOUS' def process(self, challenge=b''): return b'Anonymous, Suelta' @sasl_mech(1) class LOGIN(Mech): name = 'LOGIN' required_credentials = {'username', 'password'} def setup(self, name): self.step = 0 def process(self, challenge=b''): if not challenge: return b'' if self.step == 0: self.step = 1 return self.credentials['username'] else: return self.credentials['password'] @sasl_mech(2) class PLAIN(Mech): name = 'PLAIN' required_credentials = {'username', 'password'} optional_credentials = {'authzid'} security = {'encrypted', 'encrypted_plain', 'unencrypted_plain'} def setup(self, name): if not self.security_settings['encrypted']: if not self.security_settings['unencrypted_plain']: raise SASLCancelled('PLAIN without encryption') else: if not self.security_settings['encrypted_plain']: raise SASLCancelled('PLAIN with encryption') def process(self, challenge=b''): authzid = self.credentials['authzid'] authcid = self.credentials['username'] password = self.credentials['password'] return authzid + b'\x00' + authcid + b'\x00' + password @sasl_mech(100) class EXTERNAL(Mech): name = 'EXTERNAL' optional_credentials = {'authzid'} def process(self, challenge=b''): return self.credentials['authzid'] @sasl_mech(31) class X_FACEBOOK_PLATFORM(Mech): name = 'X-FACEBOOK-PLATFORM' required_credentials = {'api_key', 'access_token'} def process(self, challenge=b''): if challenge: values = {} for kv in challenge.split(b'&'): key, value = kv.split(b'=') values[key] = value resp_data = { b'method': values[b'method'], b'v': b'1.0', b'call_id': b'1.0', b'nonce': values[b'nonce'], b'access_token': self.credentials['access_token'], b'api_key': self.credentials['api_key'] } resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()]) return bytes(resp) return b'' @sasl_mech(10) class X_MESSENGER_OAUTH2(Mech): name = 'X-MESSENGER-OAUTH2' required_credentials = {'access_token'} def process(self, challenge=b''): return self.credentials['access_token'] @sasl_mech(10) class X_OAUTH2(Mech): name = 'X-OAUTH2' required_credentials = {'username', 'access_token'} def process(self, challenge=b''): return b'\x00' + self.credentials['username'] + \ b'\x00' + self.credentials['access_token'] @sasl_mech(3) class X_GOOGLE_TOKEN(Mech): name = 'X-GOOGLE-TOKEN' required_credentials = {'email', 'access_token'} def process(self, challenge=b''): email = self.credentials['email'] token = self.credentials['access_token'] return b'\x00' + email + b'\x00' + token @sasl_mech(20) class CRAM(Mech): name = 'CRAM' use_hashes = True required_credentials = {'username', 'password'} security = {'encrypted', 'unencrypted_cram'} def setup(self, name): self.hash_name = name[5:] self.hash = hash(self.hash_name) if self.hash is None: raise SASLCancelled('Unknown hash: %s' % self.hash_name) if not self.security_settings['encrypted']: if not self.security_settings['unencrypted_cram']: raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name) def process(self, challenge=b''): if not challenge: return None username = self.credentials['username'] password = self.credentials['password'] mac = hmac.HMAC(key=password, digestmod=self.hash) mac.update(challenge) return username + b' ' + bytes(mac.hexdigest()) @sasl_mech(60) class SCRAM(Mech): name = 'SCRAM' use_hashes = True channel_binding = True required_credentials = {'username', 'password'} optional_credentials = {'authzid', 'channel_binding'} security = {'encrypted', 'unencrypted_scram'} def setup(self, name): self.use_channel_binding = False if name[-5:] == '-PLUS': name = name[:-5] self.use_channel_binding = True self.hash_name = name[6:] self.hash = hash(self.hash_name) if self.hash is None: raise SASLCancelled('Unknown hash: %s' % self.hash_name) if not self.security_settings['encrypted']: if not self.security_settings['unencrypted_scram']: raise SASLCancelled('Unencrypted SCRAM') self.step = 0 self._mutual_auth = False def HMAC(self, key, msg): return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest() def Hi(self, text, salt, iterations): text = bytes(text) ui1 = self.HMAC(text, salt + b'\0\0\0\01') ui = ui1 for i in range(iterations - 1): ui1 = self.HMAC(text, ui1) ui = XOR(ui, ui1) return ui def H(self, text): return self.hash(text).digest() def saslname(self, value): value = value.decode("utf-8") escaped = [] for char in value: if char == ',': escaped += b'=2C' elif char == '=': escaped += b'=3D' else: escaped += char return "".join(escaped).encode("utf-8") def parse(self, challenge): items = {} for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]: items[key] = value return items def process(self, challenge=b''): steps = [self.process_1, self.process_2, self.process_3] return steps[self.step](challenge) def process_1(self, challenge): self.step = 1 data = {} self.cnonce = bytes(('%s' % random.random())[2:]) gs2_cbind_flag = b'n' if self.credentials['channel_binding']: if self.use_channel_binding: gs2_cbind_flag = b'p=tls-unique' else: gs2_cbind_flag = b'y' authzid = b'' if self.credentials['authzid']: authzid = b'a=' + self.saslname(self.credentials['authzid']) self.gs2_header = gs2_cbind_flag + b',' + authzid + b',' nonce = b'r=' + self.cnonce username = b'n=' + self.saslname(self.credentials['username']) self.client_first_message_bare = username + b',' + nonce self.client_first_message = self.gs2_header + \ self.client_first_message_bare return self.client_first_message def process_2(self, challenge): self.step = 2 data = self.parse(challenge) if b'm' in data: raise SASLCancelled('Received reserved attribute.') salt = b64decode(data[b's']) iteration_count = int(data[b'i']) nonce = data[b'r'] if nonce[:len(self.cnonce)] != self.cnonce: raise SASLCancelled('Invalid nonce') cbind_data = b'' if self.use_channel_binding: cbind_data = self.credentials['channel_binding'] cbind_input = self.gs2_header + cbind_data channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') client_final_message_without_proof = channel_binding + b',' + \ b'r=' + nonce salted_password = self.Hi(self.credentials['password'], salt, iteration_count) client_key = self.HMAC(salted_password, b'Client Key') stored_key = self.H(client_key) auth_message = self.client_first_message_bare + b',' + \ challenge + b',' + \ client_final_message_without_proof client_signature = self.HMAC(stored_key, auth_message) client_proof = XOR(client_key, client_signature) server_key = self.HMAC(salted_password, b'Server Key') self.server_signature = self.HMAC(server_key, auth_message) client_final_message = client_final_message_without_proof + \ b',p=' + b64encode(client_proof) return client_final_message def process_3(self, challenge): data = self.parse(challenge) verifier = data.get(b'v', None) error = data.get(b'e', 'Unknown error') if not verifier: raise SASLFailed(error) if b64decode(verifier) != self.server_signature: raise SASLMutualAuthFailed() self._mutual_auth = True return b'' @sasl_mech(30) class DIGEST(Mech): name = 'DIGEST' use_hashes = True required_credentials = {'username', 'password', 'realm', 'service', 'host'} optional_credentials = {'authzid', 'service-name'} security = {'encrypted', 'unencrypted_digest'} def setup(self, name): self.hash_name = name[7:] self.hash = hash(self.hash_name) if self.hash is None: raise SASLCancelled('Unknown hash: %s' % self.hash_name) if not self.security_settings['encrypted']: if not self.security_settings['unencrypted_digest']: raise SASLCancelled('Unencrypted DIGEST') self.qops = [b'auth'] self.qop = b'auth' self.maxbuf = b'65536' self.nonce = b'' self.cnonce = b'' self.nonce_count = 1 def parse(self, challenge=b''): data = {} var_name = b'' var_value = b'' # States: var, new_var, end, quote, escaped_quote state = 'var' for char in challenge: char = bytes([char]) if state == 'var': if char.isspace(): continue if char == b'=': state = 'value' else: var_name += char elif state == 'value': if char == b'"': state = 'quote' elif char == b',': if var_name: data[var_name.decode('utf-8')] = var_value var_name = b'' var_value = b'' state = 'var' else: var_value += char elif state == 'escaped': var_value += char elif state == 'quote': if char == b'\\': state = 'escaped' elif char == b'"': state = 'end' else: var_value += char else: if char == b',': if var_name: data[var_name.decode('utf-8')] = var_value var_name = b'' var_value = b'' state = 'var' else: var_value += char if var_name: data[var_name.decode('utf-8')] = var_value var_name = b'' var_value = b'' state = 'var' return data def MAC(self, key, seq, msg): mac = hmac.HMAC(key=key, digestmod=self.hash) seqnum = num_to_bytes(seq) mac.update(seqnum) mac.update(msg) return mac.digest()[:10] + b'\x00\x01' + seqnum def A1(self): username = self.credentials['username'] password = self.credentials['password'] authzid = self.credentials['authzid'] realm = self.credentials['realm'] a1 = self.hash() a1.update(username + b':' + realm + b':' + password) a1 = a1.digest() a1 += b':' + self.nonce + b':' + self.cnonce if authzid: a1 += b':' + authzid return bytes(a1) def A2(self, prefix=b''): a2 = prefix + b':' + self.digest_uri() if self.qop in (b'auth-int', b'auth-conf'): a2 += b':00000000000000000000000000000000' return bytes(a2) def response(self, prefix=b''): nc = bytes('%08x' % self.nonce_count) a1 = bytes(self.hash(self.A1()).hexdigest().lower()) a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower()) s = self.nonce + b':' + nc + b':' + self.cnonce + \ b':' + self.qop + b':' + a2 return bytes(self.hash(a1 + b':' + s).hexdigest().lower()) def digest_uri(self): serv_type = self.credentials['service'] serv_name = self.credentials['service-name'] host = self.credentials['host'] uri = serv_type + b'/' + host if serv_name and host != serv_name: uri += b'/' + serv_name return uri def respond(self): data = { 'username': quote(self.credentials['username']), 'authzid': quote(self.credentials['authzid']), 'realm': quote(self.credentials['realm']), 'nonce': quote(self.nonce), 'cnonce': quote(self.cnonce), 'nc': bytes('%08x' % self.nonce_count), 'qop': self.qop, 'digest-uri': quote(self.digest_uri()), 'response': self.response(b'AUTHENTICATE'), 'maxbuf': self.maxbuf, 'charset': 'utf-8' } resp = b'' for key, value in data.items(): if value and value != b'""': resp += b',' + bytes(key) + b'=' + bytes(value) return resp[1:] def process(self, challenge=b''): if not challenge: if self.cnonce and self.nonce and self.nonce_count and self.qop: self.nonce_count += 1 return self.respond() return None data = self.parse(challenge) if 'rspauth' in data: if data['rspauth'] != self.response(): raise SASLMutualAuthFailed() else: self.nonce_count = 1 self.cnonce = bytes('%s' % random.random())[2:] self.qops = data.get('qop', [b'auth']) self.qop = b'auth' if 'nonce' in data: self.nonce = data['nonce'] if 'realm' in data and not self.credentials['realm']: self.credentials['realm'] = data['realm'] return self.respond() try: import kerberos except ImportError: pass else: @sasl_mech(75) class GSSAPI(Mech): name = 'GSSAPI' required_credentials = {'username', 'service-name'} optional_credentials = {'authzid'} def setup(self, name): authzid = self.credentials['authzid'] if not authzid: authzid = 'xmpp@%s' % self.credentials['service-name'] _, self.gss = kerberos.authGSSClientInit(authzid) self.step = 0 def process(self, challenge=b''): b64_challenge = b64encode(challenge) try: if self.step == 0: result = kerberos.authGSSClientStep(self.gss, b64_challenge) if result != kerberos.AUTH_GSS_CONTINUE: self.step = 1 elif not challenge: kerberos.authGSSClientClean(self.gss) return b'' elif self.step == 1: username = self.credentials['username'] kerberos.authGSSClientUnwrap(self.gss, b64_challenge) resp = kerberos.authGSSClientResponse(self.gss) kerberos.authGSSClientWrap(self.gss, resp, username) resp = kerberos.authGSSClientResponse(self.gss) except kerberos.GSSError as e: raise SASLCancelled('Kerberos error: %s' % e) if not resp: return b'' else: return b64decode(resp) slixmpp-1.2.2/slixmpp/plugins/0000755000175000001440000000000013014656513017355 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0224/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0224/__init__.py0000644000175000001440000000064512770302340022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0224 import stanza from slixmpp.plugins.xep_0224.stanza import Attention from slixmpp.plugins.xep_0224.attention import XEP_0224 register_plugin(XEP_0224) slixmpp-1.2.2/slixmpp/plugins/xep_0224/attention.py0000644000175000001440000000415713004224717023202 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0224 import stanza log = logging.getLogger(__name__) class XEP_0224(BasePlugin): """ XEP-0224: Attention """ name = 'xep_0224' description = 'XEP-0224: Attention' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): """Start the XEP-0224 plugin.""" register_stanza_plugin(Message, stanza.Attention) self.xmpp.register_handler( Callback('Attention', StanzaPath('message/attention'), self._handle_attention)) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=stanza.Attention.namespace) self.xmpp.remove_handler('Attention') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace) def request_attention(self, to, mfrom=None, mbody=''): """ Send an attention message with an optional body. Arguments: to -- The attention request recipient's JID. mfrom -- Optionally specify the sender of the attention request. mbody -- An optional message body to include in the request. """ m = self.xmpp.Message() m['to'] = to m['type'] = 'headline' m['attention'] = True if mfrom: m['from'] = mfrom m['body'] = mbody m.send() def _handle_attention(self, msg): """ Raise an event after receiving a message with an attention request. Arguments: msg -- A message stanza with an attention element. """ log.debug("Received attention request from: %s", msg['from']) self.xmpp.event('attention', msg) slixmpp-1.2.2/slixmpp/plugins/xep_0224/stanza.py0000644000175000001440000000165413004224717022474 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class Attention(ElementBase): """ """ name = 'attention' namespace = 'urn:xmpp:attention:0' plugin_attrib = 'attention' interfaces = {'attention'} is_extension = True def setup(self, xml): return True def set_attention(self, value): if value: xml = ET.Element(self.tag_name()) self.parent().xml.append(xml) else: self.del_attention() def get_attention(self): xml = self.parent().xml.find(self.tag_name()) return xml is not None def del_attention(self): xml = self.parent().xml.find(self.tag_name()) if xml is not None: self.parent().xml.remove(xml) slixmpp-1.2.2/slixmpp/plugins/xep_0092/0000755000175000001440000000000013014656513020623 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0092/__init__.py0000644000175000001440000000064112770302340022727 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0092 import stanza from slixmpp.plugins.xep_0092.stanza import Version from slixmpp.plugins.xep_0092.version import XEP_0092 register_plugin(XEP_0092) slixmpp-1.2.2/slixmpp/plugins/xep_0092/version.py0000644000175000001440000000457613004224717022672 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0092 import Version, stanza log = logging.getLogger(__name__) class XEP_0092(BasePlugin): """ XEP-0092: Software Version """ name = 'xep_0092' description = 'XEP-0092: Software Version' dependencies = {'xep_0030'} stanza = stanza default_config = { 'software_name': 'Slixmpp', 'version': slixmpp.__version__, 'os': '' } def plugin_init(self): """ Start the XEP-0092 plugin. """ if 'name' in self.config: self.software_name = self.config['name'] self.xmpp.register_handler( Callback('Software Version', StanzaPath('iq@type=get/software_version'), self._handle_version)) register_stanza_plugin(Iq, Version) def plugin_end(self): self.xmpp.remove_handler('Software Version') self.xmpp['xep_0030'].del_feature(feature='jabber:iq:version') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') def _handle_version(self, iq): """ Respond to a software version query. Arguments: iq -- The Iq stanza containing the software version query. """ iq = iq.reply() iq['software_version']['name'] = self.software_name iq['software_version']['version'] = self.version iq['software_version']['os'] = self.os iq.send() def get_version(self, jid, ifrom=None, timeout=None, callback=None, timeout_callback=None): """ Retrieve the software version of a remote agent. Arguments: jid -- The JID of the entity to query. """ iq = self.xmpp.Iq() iq['to'] = jid iq['from'] = ifrom iq['type'] = 'get' iq['query'] = Version.namespace return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0092/stanza.py0000644000175000001440000000213013004224717022465 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class Version(ElementBase): """ XMPP allows for an agent to advertise the name and version of the underlying software libraries, as well as the operating system that the agent is running on. Example version stanzas: Slixmpp 1.0 Linux Stanza Interface: name -- The human readable name of the software. version -- The specific version of the software. os -- The name of the operating system running the program. """ name = 'query' namespace = 'jabber:iq:version' plugin_attrib = 'software_version' interfaces = {'name', 'version', 'os'} sub_interfaces = interfaces slixmpp-1.2.2/slixmpp/plugins/xep_0313/0000755000175000001440000000000013014656513020617 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0313/__init__.py0000644000175000001440000000060012424504520022716 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0313.stanza import Result, MAM, Preferences from slixmpp.plugins.xep_0313.mam import XEP_0313 register_plugin(XEP_0313) slixmpp-1.2.2/slixmpp/plugins/xep_0313/mam.py0000644000175000001440000000544413004224717021746 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging import slixmpp from slixmpp.stanza import Message, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Collector from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0313 import stanza log = logging.getLogger(__name__) class XEP_0313(BasePlugin): """ XEP-0313 Message Archive Management """ name = 'xep_0313' description = 'XEP-0313: Message Archive Management' dependencies = {'xep_0030', 'xep_0050', 'xep_0059', 'xep_0297'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, stanza.MAM) register_stanza_plugin(Iq, stanza.Preferences) register_stanza_plugin(Message, stanza.Result) register_stanza_plugin(Message, stanza.Archived, iterable=True) register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded) register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, timeout=None, callback=None, iterator=False): iq = self.xmpp.Iq() query_id = iq['id'] iq['to'] = jid iq['from'] = ifrom iq['type'] = 'get' iq['mam']['queryid'] = query_id iq['mam']['start'] = start iq['mam']['end'] = end iq['mam']['with'] = with_jid collector = Collector( 'MAM_Results_%s' % query_id, StanzaPath('message/mam_result@queryid=%s' % query_id)) self.xmpp.register_handler(collector) if iterator: return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results') def wrapped_cb(iq): results = collector.stop() if iq['type'] == 'result': iq['mam']['results'] = results callback(iq) return iq.send(timeout=timeout, callback=wrapped_cb) def set_preferences(self, jid=None, default=None, always=None, never=None, ifrom=None, block=True, timeout=None, callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom iq['mam_prefs']['default'] = default iq['mam_prefs']['always'] = always iq['mam_prefs']['never'] = never return iq.send(block=block, timeout=timeout, callback=callback) def get_configuration_commands(self, jid, **kwargs): return self.xmpp['xep_0030'].get_items( jid=jid, node='urn:xmpp:mam#configure', **kwargs) slixmpp-1.2.2/slixmpp/plugins/xep_0313/stanza.py0000644000175000001440000000710113010437511022457 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import datetime as dt from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins import xep_0082 class MAM(ElementBase): name = 'query' namespace = 'urn:xmpp:mam:tmp' plugin_attrib = 'mam' interfaces = {'queryid', 'start', 'end', 'with', 'results'} sub_interfaces = {'start', 'end', 'with'} def setup(self, xml=None): ElementBase.setup(self, xml) self._results = [] def get_start(self): timestamp = self._get_sub_text('start') return xep_0082.parse(timestamp) def set_start(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_sub_text('start', value) def get_end(self): timestamp = self._get_sub_text('end') return xep_0082.parse(timestamp) def set_end(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_sub_text('end', value) def get_with(self): return JID(self._get_sub_text('with')) def set_with(self, value): self._set_sub_text('with', str(value)) # The results interface is meant only as an easy # way to access the set of collected message responses # from the query. def get_results(self): return self._results def set_results(self, values): self._results = values def del_results(self): self._results = [] class Preferences(ElementBase): name = 'prefs' namespace = 'urn:xmpp:mam:tmp' plugin_attrib = 'mam_prefs' interfaces = {'default', 'always', 'never'} sub_interfaces = {'always', 'never'} def get_always(self): results = set() jids = self.xml.findall('{%s}always/{%s}jid' % ( self.namespace, self.namespace)) for jid in jids: results.add(JID(jid.text)) return results def set_always(self, value): self._set_sub_text('always', '', keep=True) always = self.xml.find('{%s}always' % self.namespace) always.clear() if not isinstance(value, (list, set)): value = [value] for jid in value: jid_xml = ET.Element('{%s}jid' % self.namespace) jid_xml.text = str(jid) always.append(jid_xml) def get_never(self): results = set() jids = self.xml.findall('{%s}never/{%s}jid' % ( self.namespace, self.namespace)) for jid in jids: results.add(JID(jid.text)) return results def set_never(self, value): self._set_sub_text('never', '', keep=True) never = self.xml.find('{%s}never' % self.namespace) never.clear() if not isinstance(value, (list, set)): value = [value] for jid in value: jid_xml = ET.Element('{%s}jid' % self.namespace) jid_xml.text = str(jid) never.append(jid_xml) class Result(ElementBase): name = 'result' namespace = 'urn:xmpp:mam:tmp' plugin_attrib = 'mam_result' interfaces = {'queryid', 'id'} class Archived(ElementBase): name = 'archived' namespace = 'urn:xmpp:mam:tmp' plugin_attrib = 'mam_archived' plugin_multi_attrib = 'mam_archives' interfaces = {'by', 'id'} def get_by(self): return JID(self._get_attr('by')) def set_by(self, value): return self._set_attr('by', str(value)) slixmpp-1.2.2/slixmpp/plugins/xep_0196/0000755000175000001440000000000013014656513020630 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0196/user_gaming.py0000644000175000001440000000765713004224717023515 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0196 import stanza, UserGaming log = logging.getLogger(__name__) class XEP_0196(BasePlugin): """ XEP-0196: User Gaming """ name = 'xep_0196' description = 'XEP-0196: User Gaming' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace) self.xmpp['xep_0163'].remove_interest(UserGaming.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming) def publish_gaming(self, name=None, level=None, server_name=None, uri=None, character_name=None, character_profile=None, server_address=None, options=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Publish the user's current gaming status. Arguments: name -- The name of the game. level -- The user's level in the game. uri -- A URI for the game or relevant gaming service server_name -- The name of the server where the user is playing. server_address -- The hostname or IP address of the server where the user is playing. character_name -- The name of the user's character in the game. character_profile -- A URI for a profile of the user's character. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ gaming = UserGaming() gaming['name'] = name gaming['level'] = level gaming['uri'] = uri gaming['character_name'] = character_name gaming['character_profile'] = character_profile gaming['server_name'] = server_name gaming['server_address'] = server_address return self.xmpp['xep_0163'].publish(gaming, node=UserGaming.namespace, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing user gaming information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ gaming = UserGaming() return self.xmpp['xep_0163'].publish(gaming, node=UserGaming.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0196/__init__.py0000644000175000001440000000065012424504520022734 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0196 import stanza from slixmpp.plugins.xep_0196.stanza import UserGaming from slixmpp.plugins.xep_0196.user_gaming import XEP_0196 register_plugin(XEP_0196) slixmpp-1.2.2/slixmpp/plugins/xep_0196/stanza.py0000644000175000001440000000100713004224717022474 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class UserGaming(ElementBase): name = 'gaming' namespace = 'urn:xmpp:gaming:0' plugin_attrib = 'gaming' interfaces = {'character_name', 'character_profile', 'name', 'level', 'server_address', 'server_name', 'uri'} sub_interfaces = interfaces slixmpp-1.2.2/slixmpp/plugins/xep_0108/0000755000175000001440000000000013014656513020621 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0108/__init__.py0000644000175000001440000000065412424504520022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0108 import stanza from slixmpp.plugins.xep_0108.stanza import UserActivity from slixmpp.plugins.xep_0108.user_activity import XEP_0108 register_plugin(XEP_0108) slixmpp-1.2.2/slixmpp/plugins/xep_0108/user_activity.py0000644000175000001440000000631013004224717024061 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0108 import stanza, UserActivity log = logging.getLogger(__name__) class XEP_0108(BasePlugin): """ XEP-0108: User Activity """ name = 'xep_0108' description = 'XEP-0108: User Activity' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserActivity.namespace) self.xmpp['xep_0163'].remove_interest(UserActivity.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_activity', UserActivity) def publish_activity(self, general, specific=None, text=None, options=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Publish the user's current activity. Arguments: general -- The required general category of the activity. specific -- Optional specific activity being done as part of the general category. text -- Optional natural-language description or reason for the activity. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ activity = UserActivity() activity['value'] = (general, specific) activity['text'] = text self.xmpp['xep_0163'].publish(activity, node=UserActivity.namespace, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing user activity information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ activity = UserActivity() self.xmpp['xep_0163'].publish(activity, node=UserActivity.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0108/stanza.py0000644000175000001440000000662313004224717022476 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class UserActivity(ElementBase): name = 'activity' namespace = 'http://jabber.org/protocol/activity' plugin_attrib = 'activity' interfaces = {'value', 'text'} sub_interfaces = {'text'} general = {'doing_chores', 'drinking', 'eating', 'exercising', 'grooming', 'having_appointment', 'inactive', 'relaxing', 'talking', 'traveling', 'undefined', 'working'} specific = {'at_the_spa', 'brushing_teeth', 'buying_groceries', 'cleaning', 'coding', 'commuting', 'cooking', 'cycling', 'dancing', 'day_off', 'doing_maintenance', 'doing_the_dishes', 'doing_the_laundry', 'driving', 'fishing', 'gaming', 'gardening', 'getting_a_haircut', 'going_out', 'hanging_out', 'having_a_beer', 'having_a_snack', 'having_breakfast', 'having_coffee', 'having_dinner', 'having_lunch', 'having_tea', 'hiding', 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life', 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train', 'on_a_trip', 'on_the_phone', 'on_vacation', 'on_video_phone', 'other', 'partying', 'playing_sports', 'praying', 'reading', 'rehearsing', 'running', 'running_an_errand', 'scheduled_holiday', 'shaving', 'shopping', 'skiing', 'sleeping', 'smoking', 'socializing', 'studying', 'sunbathing', 'swimming', 'taking_a_bath', 'taking_a_shower', 'thinking', 'walking', 'walking_the_dog', 'watching_a_movie', 'watching_tv', 'working_out', 'writing'} def set_value(self, value): self.del_value() general = value specific = None if isinstance(value, tuple) or isinstance(value, list): general = value[0] specific = value[1] if general in self.general: gen_xml = ET.Element('{%s}%s' % (self.namespace, general)) if specific: spec_xml = ET.Element('{%s}%s' % (self.namespace, specific)) if specific in self.specific: gen_xml.append(spec_xml) else: raise ValueError('Unknown specific activity') self.xml.append(gen_xml) else: raise ValueError('Unknown general activity') def get_value(self): general = None specific = None gen_xml = None for child in self.xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.general: general = elem_name gen_xml = child if gen_xml is not None: for child in gen_xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.specific: specific = elem_name return (general, specific) def del_value(self): curr_value = self.get_value() if curr_value[0]: self._set_sub_text(curr_value[0], '', keep=False) slixmpp-1.2.2/slixmpp/plugins/xep_0256/0000755000175000001440000000000013014656513020625 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0256/last_activity_presence.py0000644000175000001440000000122713004224717025740 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Presence from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0012 import stanza, LastActivity log = logging.getLogger(__name__) class XEP_0256(BasePlugin): name = 'xep_0256' description = 'XEP-0256: Last Activity in Presence' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Presence, LastActivity) slixmpp-1.2.2/slixmpp/plugins/xep_0256/__init__.py0000644000175000001440000000053512724665434022752 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0256.last_activity_presence import XEP_0256, LastActivity register_plugin(XEP_0256) slixmpp-1.2.2/slixmpp/plugins/xep_0153/0000755000175000001440000000000013014656513020621 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0153/vcard_avatar.py0000644000175000001440000001476613004224717023642 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import hashlib import logging from slixmpp.stanza import Presence from slixmpp.exceptions import XMPPError, IqTimeout from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0153 import stanza, VCardTempUpdate from slixmpp import asyncio, future_wrapper log = logging.getLogger(__name__) class XEP_0153(BasePlugin): name = 'xep_0153' description = 'XEP-0153: vCard-Based Avatars' dependencies = {'xep_0054'} stanza = stanza def plugin_init(self): self._hashes = {} register_stanza_plugin(Presence, VCardTempUpdate) self.xmpp.add_filter('out', self._update_presence) self.xmpp.add_event_handler('session_start', self._start) self.xmpp.add_event_handler('session_end', self._end) self.xmpp.add_event_handler('presence_available', self._recv_presence) self.xmpp.add_event_handler('presence_dnd', self._recv_presence) self.xmpp.add_event_handler('presence_xa', self._recv_presence) self.xmpp.add_event_handler('presence_chat', self._recv_presence) self.xmpp.add_event_handler('presence_away', self._recv_presence) self.api.register(self._set_hash, 'set_hash', default=True) self.api.register(self._get_hash, 'get_hash', default=True) self.api.register(self._reset_hash, 'reset_hash', default=True) def plugin_end(self): self.xmpp.del_filter('out', self._update_presence) self.xmpp.del_event_handler('session_start', self._start) self.xmpp.del_event_handler('session_end', self._end) self.xmpp.del_event_handler('presence_available', self._recv_presence) self.xmpp.del_event_handler('presence_dnd', self._recv_presence) self.xmpp.del_event_handler('presence_xa', self._recv_presence) self.xmpp.del_event_handler('presence_chat', self._recv_presence) self.xmpp.del_event_handler('presence_away', self._recv_presence) @future_wrapper def set_avatar(self, jid=None, avatar=None, mtype=None, timeout=None, callback=None, timeout_callback=None): if jid is None: jid = self.xmpp.boundjid.bare future = asyncio.Future() def propagate_timeout_exception(fut): try: fut.done() except IqTimeout as e: future.set_exception(e) def custom_callback(result): vcard = result['vcard_temp'] vcard['PHOTO']['TYPE'] = mtype vcard['PHOTO']['BINVAL'] = avatar new_future = self.xmpp['xep_0054'].publish_vcard(jid=jid, vcard=vcard, timeout=timeout, callback=next_callback, timeout_callback=timeout_callback) new_future.add_done_callback(propagate_timeout_exception) def next_callback(result): if result['type'] == 'error': future.set_exception(result) else: self.api['reset_hash'](jid) self.xmpp.roster[jid].send_last_presence() future.set_result(result) first_future = self.xmpp['xep_0054'].get_vcard(jid, cached=False, timeout=timeout, callback=custom_callback, timeout_callback=timeout_callback) first_future.add_done_callback(propagate_timeout_exception) return future @asyncio.coroutine def _start(self, event): try: vcard = yield from self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare) data = vcard['vcard_temp']['PHOTO']['BINVAL'] if not data: new_hash = '' else: new_hash = hashlib.sha1(data).hexdigest() self.api['set_hash'](self.xmpp.boundjid, args=new_hash) except XMPPError: log.debug('Could not retrieve vCard for %s', self.xmpp.boundjid.bare) def _end(self, event): pass def _update_presence(self, stanza): if not isinstance(stanza, Presence): return stanza if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'): return stanza current_hash = self.api['get_hash'](stanza['from']) stanza['vcard_temp_update']['photo'] = current_hash return stanza def _reset_hash(self, jid, node, ifrom, args): own_jid = (jid.bare == self.xmpp.boundjid.bare) if self.xmpp.is_component: own_jid = (jid.domain == self.xmpp.boundjid.domain) self.api['set_hash'](jid, args=None) if own_jid: self.xmpp.roster[jid].send_last_presence() def callback(iq): if iq['type'] == 'error': log.debug('Could not retrieve vCard for %s', jid) return data = iq['vcard_temp']['PHOTO']['BINVAL'] if not data: new_hash = '' else: new_hash = hashlib.sha1(data).hexdigest() self.api['set_hash'](jid, args=new_hash) self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom, callback=callback) def _recv_presence(self, pres): try: if pres['muc']['affiliation']: # Don't process vCard avatars for MUC occupants # since they all share the same bare JID. return except: pass if not pres.match('presence/vcard_temp_update'): self.api['set_hash'](pres['from'], args=None) return data = pres['vcard_temp_update']['photo'] if data is None: return elif data == '' or data != self.api['get_hash'](pres['from']): ifrom = pres['to'] if self.xmpp.is_component else None self.api['reset_hash'](pres['from'], ifrom=ifrom) self.xmpp.event('vcard_avatar_update', pres) # ================================================================= def _get_hash(self, jid, node, ifrom, args): return self._hashes.get(jid.bare, None) def _set_hash(self, jid, node, ifrom, args): self._hashes[jid.bare] = args slixmpp-1.2.2/slixmpp/plugins/xep_0153/__init__.py0000644000175000001440000000060212424504520022722 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0153.stanza import VCardTempUpdate from slixmpp.plugins.xep_0153.vcard_avatar import XEP_0153 register_plugin(XEP_0153) slixmpp-1.2.2/slixmpp/plugins/xep_0153/stanza.py0000644000175000001440000000136113004224717022470 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class VCardTempUpdate(ElementBase): name = 'x' namespace = 'vcard-temp:x:update' plugin_attrib = 'vcard_temp_update' interfaces = {'photo'} sub_interfaces = interfaces def set_photo(self, value): if value is not None: self._set_sub_text('photo', value, keep=True) else: self._del_sub('photo') def get_photo(self): photo = self.xml.find('{%s}photo' % self.namespace) if photo is None: return None return photo.text slixmpp-1.2.2/slixmpp/plugins/xep_0049/0000755000175000001440000000000013014656513020625 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0049/__init__.py0000644000175000001440000000060012424504520022724 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0049.stanza import PrivateXML from slixmpp.plugins.xep_0049.private_storage import XEP_0049 register_plugin(XEP_0049) slixmpp-1.2.2/slixmpp/plugins/xep_0049/private_storage.py0000644000175000001440000000310413004224717024367 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0049 import stanza, PrivateXML log = logging.getLogger(__name__) class XEP_0049(BasePlugin): name = 'xep_0049' description = 'XEP-0049: Private XML Storage' dependencies = {} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, PrivateXML) def register(self, stanza): register_stanza_plugin(PrivateXML, stanza, iterable=True) def store(self, data, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom if not isinstance(data, list): data = [data] for elem in data: iq['private'].append(elem) return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def retrieve(self, name, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['from'] = ifrom iq['private'].enable(name) return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0049/stanza.py0000644000175000001440000000057112424504520022474 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ET, ElementBase class PrivateXML(ElementBase): name = 'query' namespace = 'jabber:iq:private' plugin_attrib = 'private' interfaces = set() slixmpp-1.2.2/slixmpp/plugins/xep_0222.py0000644000175000001440000001050413004226027021160 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin, register_plugin log = logging.getLogger(__name__) class XEP_0222(BasePlugin): """ XEP-0222: Persistent Storage of Public Data via PubSub """ name = 'xep_0222' description = 'XEP-0222: Persistent Storage of Public Data via PubSub' dependencies = {'xep_0163', 'xep_0060', 'xep_0004'} profile = {'pubsub#persist_items': True, 'pubsub#send_last_published_item': 'never'} def configure(self, node, ifrom=None, callback=None, timeout=None): """ Update a node's configuration to match the public storage profile. """ config = self.xmpp['xep_0004'].Form() config['type'] = 'submit' for field, value in self.profile.items(): config.add_field(var=field, value=value) return self.xmpp['xep_0060'].set_node_config(None, node, config, ifrom=ifrom, callback=callback, timeout=timeout) def store(self, stanza, node=None, id=None, ifrom=None, options=None, callback=None, timeout=None): """ Store public data via PEP. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. Arguments: stanza -- The private content to store. node -- The node to publish the content to. If not specified, the stanza's namespace will be used. id -- Optionally specify the ID of the item. options -- Publish options to use, which will be modified to fit the persistent storage option profile. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if not options: options = self.xmpp['xep_0004'].stanza.Form() options['type'] = 'submit' options.add_field( var='FORM_TYPE', ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') fields = options['fields'] for field, value in self.profile.items(): if field not in fields: options.add_field(var=field) options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, ifrom=ifrom, callback=callback, timeout=timeout) def retrieve(self, node, id=None, item_ids=None, ifrom=None, callback=None, timeout=None): """ Retrieve public data via PEP. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. Arguments: node -- The node to retrieve content from. id -- Optionally specify the ID of the item. item_ids -- Specify a group of IDs. If id is also specified, it will be included in item_ids. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if item_ids is None: item_ids = [] if id is not None: item_ids.append(id) return self.xmpp['xep_0060'].get_items(None, node, item_ids=item_ids, ifrom=ifrom, callback=callback, timeout=timeout) register_plugin(XEP_0222) slixmpp-1.2.2/slixmpp/plugins/xep_0050/0000755000175000001440000000000013014656513020615 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0050/__init__.py0000644000175000001440000000056312770302340022724 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0050.stanza import Command from slixmpp.plugins.xep_0050.adhoc import XEP_0050 register_plugin(XEP_0050) slixmpp-1.2.2/slixmpp/plugins/xep_0050/adhoc.py0000644000175000001440000006010713004224717022245 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import time from slixmpp import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0050 import stanza from slixmpp.plugins.xep_0050 import Command from slixmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) class XEP_0050(BasePlugin): """ XEP-0050: Ad-Hoc Commands XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Events: command_execute -- Received a command with action="execute" command_next -- Received a command with action="next" command_complete -- Received a command with action="complete" command_cancel -- Received a command with action="cancel" Attributes: commands -- A dictionary mapping JID/node pairs to command names and handlers. sessions -- A dictionary or equivalent backend mapping session IDs to dictionaries containing data relevant to a command's session. Methods: plugin_init -- Overrides BasePlugin.plugin_init post_init -- Overrides BasePlugin.post_init new_session -- Return a new session ID. prep_handlers -- Placeholder. May call with a list of handlers to prepare them for use with the session storage backend, if needed. set_backend -- Replace the default session storage with some external storage mechanism, such as a database. The provided backend wrapper must be able to act using the same syntax as a dictionary. add_command -- Add a command for use by external entitites. get_commands -- Retrieve a list of commands provided by a remote agent. send_command -- Send a command request to a remote agent. start_command -- Command user API: initiate a command session continue_command -- Command user API: proceed to the next step cancel_command -- Command user API: cancel a command complete_command -- Command user API: finish a command terminate_command -- Command user API: delete a command's session """ name = 'xep_0050' description = 'XEP-0050: Ad-Hoc Commands' dependencies = {'xep_0030', 'xep_0004'} stanza = stanza default_config = { 'session_db': None } def plugin_init(self): """Start the XEP-0050 plugin.""" self.sessions = self.session_db if self.sessions is None: self.sessions = {} self.commands = {} self.xmpp.register_handler( Callback("Ad-Hoc Execute", StanzaPath('iq@type=set/command'), self._handle_command)) register_stanza_plugin(Iq, Command) register_stanza_plugin(Command, Form, iterable=True) self.xmpp.add_event_handler('command_execute', self._handle_command_start) self.xmpp.add_event_handler('command_next', self._handle_command_next) self.xmpp.add_event_handler('command_cancel', self._handle_command_cancel) self.xmpp.add_event_handler('command_complete', self._handle_command_complete) def plugin_end(self): self.xmpp.del_event_handler('command_execute', self._handle_command_start) self.xmpp.del_event_handler('command_next', self._handle_command_next) self.xmpp.del_event_handler('command_cancel', self._handle_command_cancel) self.xmpp.del_event_handler('command_complete', self._handle_command_complete) self.xmpp.remove_handler('Ad-Hoc Execute') self.xmpp['xep_0030'].del_feature(feature=Command.namespace) self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Command.namespace) self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def set_backend(self, db): """ Replace the default session storage dictionary with a generic, external data storage mechanism. The replacement backend must be able to interact through the same syntax and interfaces as a normal dictionary. Arguments: db -- The new session storage mechanism. """ self.sessions = db def prep_handlers(self, handlers, **kwargs): """ Prepare a list of functions for use by the backend service. Intended to be replaced by the backend service as needed. Arguments: handlers -- A list of function pointers **kwargs -- Any additional parameters required by the backend. """ pass # ================================================================= # Server side (command provider) API def add_command(self, jid=None, node=None, name='', handler=None): """ Make a new command available to external entities. Access control may be implemented in the provided handler. Command workflow is done across a sequence of command handlers. The first handler is given the initial Iq stanza of the request in order to support access control. Subsequent handlers are given only the payload items of the command. All handlers will receive the command's session data. Arguments: jid -- The JID that will expose the command. node -- The node associated with the command. name -- A human readable name for the command. handler -- A function that will generate the response to the initial command request, as well as enforcing any access control policies. """ if jid is None: jid = self.xmpp.boundjid elif not isinstance(jid, JID): jid = JID(jid) item_jid = jid.full self.xmpp['xep_0030'].add_identity(category='automation', itype='command-list', name='Ad-Hoc commands', node=Command.namespace, jid=jid) self.xmpp['xep_0030'].add_item(jid=item_jid, name=name, node=Command.namespace, subnode=node, ijid=jid) self.xmpp['xep_0030'].add_identity(category='automation', itype='command-node', name=name, node=node, jid=jid) self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid) self.commands[(item_jid, node)] = (name, handler) def new_session(self): """Return a new session ID.""" return str(time.time()) + '-' + self.xmpp.new_id() def _handle_command(self, iq): """Raise command events based on the command action.""" self.xmpp.event('command_%s' % iq['command']['action'], iq) def _handle_command_start(self, iq): """ Process an initial request to execute a command. Arguments: iq -- The command execution request. """ sessionid = self.new_session() node = iq['command']['node'] key = (iq['to'].full, node) name, handler = self.commands.get(key, ('Not found', None)) if not handler: log.debug('Command not found: %s, %s', key, self.commands) raise XMPPError('item-not-found') payload = [] for stanza in iq['command']['substanzas']: payload.append(stanza) if len(payload) == 1: payload = payload[0] interfaces = {item.plugin_attrib for item in payload} payload_classes = {item.__class__ for item in payload} initial_session = {'id': sessionid, 'from': iq['from'], 'to': iq['to'], 'node': node, 'payload': payload, 'interfaces': interfaces, 'payload_classes': payload_classes, 'notes': None, 'has_next': False, 'allow_complete': False, 'allow_prev': False, 'past': [], 'next': None, 'prev': None, 'cancel': None} session = handler(iq, initial_session) self._process_command_response(iq, session) def _handle_command_next(self, iq): """ Process a request for the next step in the workflow for a command with multiple steps. Arguments: iq -- The command continuation request. """ sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] session = handler(results, session) self._process_command_response(iq, session) else: raise XMPPError('item-not-found') def _handle_command_prev(self, iq): """ Process a request for the prev step in the workflow for a command with multiple steps. Arguments: iq -- The command continuation request. """ sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['prev'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] session = handler(results, session) self._process_command_response(iq, session) else: raise XMPPError('item-not-found') def _process_command_response(self, iq, session): """ Generate a command reply stanza based on the provided session data. Arguments: iq -- The command request stanza. session -- A dictionary of relevant session data. """ sessionid = session['id'] payload = session['payload'] if payload is None: payload = [] if not isinstance(payload, list): payload = [payload] interfaces = session.get('interfaces', set()) payload_classes = session.get('payload_classes', set()) interfaces.update({item.plugin_attrib for item in payload}) payload_classes.update({item.__class__ for item in payload}) session['interfaces'] = interfaces session['payload_classes'] = payload_classes self.sessions[sessionid] = session for item in payload: register_stanza_plugin(Command, item.__class__, iterable=True) iq = iq.reply() iq['command']['node'] = session['node'] iq['command']['sessionid'] = session['id'] if session['next'] is None: iq['command']['actions'] = [] iq['command']['status'] = 'completed' elif session['has_next']: actions = ['next'] if session['allow_complete']: actions.append('complete') if session['allow_prev']: actions.append('prev') iq['command']['actions'] = actions iq['command']['status'] = 'executing' else: iq['command']['actions'] = ['complete'] iq['command']['status'] = 'executing' iq['command']['notes'] = session['notes'] for item in payload: iq['command'].append(item) iq.send() def _handle_command_cancel(self, iq): """ Process a request to cancel a command's execution. Arguments: iq -- The command cancellation request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['cancel'] if handler: handler(iq, session) del self.sessions[sessionid] iq = iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['status'] = 'canceled' iq['command']['notes'] = session['notes'] iq.send() else: raise XMPPError('item-not-found') def _handle_command_complete(self, iq): """ Process a request to finish the execution of command and terminate the workflow. All data related to the command session will be removed. Arguments: iq -- The command completion request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] if handler: handler(results, session) del self.sessions[sessionid] payload = session['payload'] if payload is None: payload = [] if not isinstance(payload, list): payload = [payload] for item in payload: register_stanza_plugin(Command, item.__class__, iterable=True) iq = iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['actions'] = [] iq['command']['status'] = 'completed' iq['command']['notes'] = session['notes'] for item in payload: iq['command'].append(item) iq.send() else: raise XMPPError('item-not-found') # ================================================================= # Client side (command user) API def get_commands(self, jid, **kwargs): """ Return a list of commands provided by a given JID. Arguments: jid -- The JID to query for commands. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. ifrom -- Specifiy the sender's JID. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ return self.xmpp['xep_0030'].get_items(jid=jid, node=Command.namespace, **kwargs) def send_command(self, jid, node, ifrom=None, action='execute', payload=None, sessionid=None, flow=False, **kwargs): """ Create and send a command stanza, without using the provided workflow management APIs. Arguments: jid -- The JID to send the command request or result. node -- The node for the command. ifrom -- Specify the sender's JID. action -- May be one of: execute, cancel, complete, or cancel. payload -- Either a list of payload items, or a single payload item such as a data form. sessionid -- The current session's ID value. flow -- If True, process the Iq result using the command workflow methods contained in the session instead of returning the response stanza itself. Defaults to False. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received if flow=False. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = action if sessionid is not None: iq['command']['sessionid'] = sessionid if payload is not None: if not isinstance(payload, list): payload = [payload] for item in payload: iq['command'].append(item) if not flow: return iq.send(**kwargs) else: iq.send(callback=self._handle_command_result) def start_command(self, jid, node, session, ifrom=None): """ Initiate executing a command provided by a remote agent. The provided session dictionary should contain: next -- A handler for processing the command result. error -- A handler for processing any error stanzas generated by the request. Arguments: jid -- The JID to send the command request. node -- The node for the desired command. session -- A dictionary of relevant session data. ifrom -- Optionally specify the sender's JID. """ session['jid'] = jid session['node'] = node session['timestamp'] = time.time() if 'payload' not in session: session['payload'] = None iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom session['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = 'execute' if session['payload'] is not None: payload = session['payload'] if not isinstance(payload, list): payload = list(payload) for stanza in payload: iq['command'].append(stanza) sessionid = 'client:pending_' + iq['id'] session['id'] = sessionid self.sessions[sessionid] = session iq.send(callback=self._handle_command_result) def continue_command(self, session, direction='next'): """ Execute the next action of the command. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action=direction, payload=session.get('payload', None), sessionid=session['id'], flow=True) def cancel_command(self, session): """ Cancel the execution of a command. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='cancel', payload=session.get('payload', None), sessionid=session['id'], flow=True) def complete_command(self, session): """ Finish the execution of a command workflow. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='complete', payload=session.get('payload', None), sessionid=session['id'], flow=True) def terminate_command(self, session): """ Delete a command's session after a command has completed or an error has occured. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] try: del self.sessions[sessionid] except Exception as e: log.error("Error deleting adhoc command session: %s" % e.message) def _handle_command_result(self, iq): """ Process the results of a command request. Will execute the 'next' handler stored in the session data, or the 'error' handler depending on the Iq's type. Arguments: iq -- The command response. """ sessionid = 'client:' + iq['command']['sessionid'] pending = False if sessionid not in self.sessions: pending = True pendingid = 'client:pending_' + iq['id'] if pendingid not in self.sessions: return sessionid = pendingid session = self.sessions[sessionid] sessionid = 'client:' + iq['command']['sessionid'] session['id'] = iq['command']['sessionid'] self.sessions[sessionid] = session if pending: del self.sessions[pendingid] handler_type = 'next' if iq['type'] == 'error': handler_type = 'error' handler = session.get(handler_type, None) if handler: handler(iq, session) elif iq['type'] == 'error': self.terminate_command(session) if iq['command']['status'] == 'completed': self.terminate_command(session) slixmpp-1.2.2/slixmpp/plugins/xep_0050/stanza.py0000644000175000001440000001411313004224717022463 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class Command(ElementBase): """ XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Example command stanzas: Information! Stanza Interface: action -- The action to perform. actions -- The set of allowable next actions. node -- The node associated with the command. notes -- A list of tuples for informative notes. sessionid -- A unique identifier for a command session. status -- May be one of: canceled, completed, or executing. Attributes: actions -- A set of allowed action values. statuses -- A set of allowed status values. next_actions -- A set of allowed next action names. Methods: get_action -- Return the requested action. get_actions -- Return the allowable next actions. set_actions -- Set the allowable next actions. del_actions -- Remove the current set of next actions. get_notes -- Return a list of informative note data. set_notes -- Set informative notes. del_notes -- Remove any note data. add_note -- Add a single note. """ name = 'command' namespace = 'http://jabber.org/protocol/commands' plugin_attrib = 'command' interfaces = {'action', 'sessionid', 'node', 'status', 'actions', 'notes'} actions = {'cancel', 'complete', 'execute', 'next', 'prev'} statuses = {'canceled', 'completed', 'executing'} next_actions = {'prev', 'next', 'complete'} def get_action(self): """ Return the value of the action attribute. If the Iq stanza's type is "set" then use a default value of "execute". """ if self.parent()['type'] == 'set': return self._get_attr('action', default='execute') return self._get_attr('action') def set_actions(self, values): """ Assign the set of allowable next actions. Arguments: values -- A list containing any combination of: 'prev', 'next', and 'complete' """ self.del_actions() if values: self._set_sub_text('{%s}actions' % self.namespace, '', True) actions = self.xml.find('{%s}actions' % self.namespace) for val in values: if val in self.next_actions: action = ET.Element('{%s}%s' % (self.namespace, val)) actions.append(action) def get_actions(self): """ Return the set of allowable next actions. """ actions = set() actions_xml = self.xml.find('{%s}actions' % self.namespace) if actions_xml is not None: for action in self.next_actions: action_xml = actions_xml.find('{%s}%s' % (self.namespace, action)) if action_xml is not None: actions.add(action) return actions def del_actions(self): """ Remove all allowable next actions. """ self._del_sub('{%s}actions' % self.namespace) def get_notes(self): """ Return a list of note information. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] """ notes = [] notes_xml = self.xml.findall('{%s}note' % self.namespace) for note in notes_xml: notes.append((note.attrib.get('type', 'info'), note.text)) return notes def set_notes(self, notes): """ Add multiple notes to the command result. Each note is a tuple, with the first item being one of: 'info', 'warning', or 'error', and the second item being any human readable message. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] Arguments: notes -- A list of tuples of note information. """ self.del_notes() for note in notes: self.add_note(note[1], note[0]) def del_notes(self): """ Remove all notes associated with the command result. """ notes_xml = self.xml.findall('{%s}note' % self.namespace) for note in notes_xml: self.xml.remove(note) def add_note(self, msg='', ntype='info'): """ Add a single note annotation to the command. Arguments: msg -- A human readable message. ntype -- One of: 'info', 'warning', 'error' """ xml = ET.Element('{%s}note' % self.namespace) xml.attrib['type'] = ntype xml.text = msg self.xml.append(xml) slixmpp-1.2.2/slixmpp/plugins/xep_0308/0000755000175000001440000000000013014656513020623 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0308/__init__.py0000644000175000001440000000056612424504520022735 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0308.stanza import Replace from slixmpp.plugins.xep_0308.correction import XEP_0308 register_plugin(XEP_0308) slixmpp-1.2.2/slixmpp/plugins/xep_0308/correction.py0000644000175000001440000000256013004224717023343 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging import slixmpp from slixmpp.stanza import Message from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0308 import stanza, Replace log = logging.getLogger(__name__) class XEP_0308(BasePlugin): """ XEP-0308 Last Message Correction """ name = 'xep_0308' description = 'XEP-0308: Last Message Correction' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Message Correction', StanzaPath('message/replace'), self._handle_correction)) register_stanza_plugin(Message, Replace) self.xmpp.use_message_ids = True def plugin_end(self): self.xmpp.remove_handler('Message Correction') self.xmpp.plugin['xep_0030'].del_feature(feature=Replace.namespace) def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace) def _handle_correction(self, msg): self.xmpp.event('message_correction', msg) slixmpp-1.2.2/slixmpp/plugins/xep_0308/stanza.py0000644000175000001440000000057313004224717022476 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.xmlstream import ElementBase class Replace(ElementBase): name = 'replace' namespace = 'urn:xmpp:message-correct:0' plugin_attrib = 'replace' interfaces = {'id'} slixmpp-1.2.2/slixmpp/plugins/xep_0249/0000755000175000001440000000000013014656513020627 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0249/__init__.py0000644000175000001440000000055012770302340022732 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0249.stanza import Invite from slixmpp.plugins.xep_0249.invite import XEP_0249 register_plugin(XEP_0249) slixmpp-1.2.2/slixmpp/plugins/xep_0249/stanza.py0000644000175000001440000000216412424504520022476 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Invite(ElementBase): """ XMPP allows for an agent in an MUC room to directly invite another user to join the chat room (as opposed to a mediated invitation done through the server). Example invite stanza: Stanza Interface: jid -- The JID of the groupchat room password -- The password used to gain entry in the room (optional) reason -- The reason for the invitation (optional) """ name = "x" namespace = "jabber:x:conference" plugin_attrib = "groupchat_invite" interfaces = ("jid", "password", "reason") slixmpp-1.2.2/slixmpp/plugins/xep_0249/invite.py0000644000175000001440000000474713004224717022507 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp import Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0249 import Invite, stanza log = logging.getLogger(__name__) class XEP_0249(BasePlugin): """ XEP-0249: Direct MUC Invitations """ name = 'xep_0249' description = 'XEP-0249: Direct MUC Invitations' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Direct MUC Invitations', StanzaPath('message/groupchat_invite'), self._handle_invite)) register_stanza_plugin(Message, Invite) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Invite.namespace) self.xmpp.remove_handler('Direct MUC Invitations') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Invite.namespace) def _handle_invite(self, msg): """ Raise an event for all invitations received. """ log.debug("Received direct muc invitation from %s to room %s", msg['from'], msg['groupchat_invite']['jid']) self.xmpp.event('groupchat_direct_invite', msg) def send_invitation(self, jid, roomjid, password=None, reason=None, ifrom=None): """ Send a direct MUC invitation to an XMPP entity. Arguments: jid -- The JID of the entity that will receive the invitation roomjid -- the address of the groupchat room to be joined password -- a password needed for entry into a password-protected room (OPTIONAL). reason -- a human-readable purpose for the invitation (OPTIONAL). """ msg = self.xmpp.Message() msg['to'] = jid if ifrom is not None: msg['from'] = ifrom msg['groupchat_invite']['jid'] = roomjid if password is not None: msg['groupchat_invite']['password'] = password if reason is not None: msg['groupchat_invite']['reason'] = reason return msg.send() slixmpp-1.2.2/slixmpp/plugins/xep_0030/0000755000175000001440000000000013014656513020613 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0030/__init__.py0000644000175000001440000000074512770302340022724 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0030 import stanza from slixmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems from slixmpp.plugins.xep_0030.static import StaticDisco from slixmpp.plugins.xep_0030.disco import XEP_0030 register_plugin(XEP_0030) slixmpp-1.2.2/slixmpp/plugins/xep_0030/disco.py0000644000175000001440000007253112760131555022277 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp import future_wrapper from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems from slixmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) class XEP_0030(BasePlugin): """ XEP-0030: Service Discovery Service discovery in XMPP allows entities to discover information about other agents in the network, such as the feature sets supported by a client, or signposts to other, related entities. Also see . The XEP-0030 plugin works using a hierarchy of dynamic node handlers, ranging from global handlers to specific JID+node handlers. The default set of handlers operate in a static manner, storing disco information in memory. However, custom handlers may use any available backend storage mechanism desired, such as SQLite or Redis. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Stream Handlers: Disco Info -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#info. Disco Items -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#items. Events: disco_info -- Received a disco#info Iq query result. disco_items -- Received a disco#items Iq query result. disco_info_query -- Received a disco#info Iq query request. disco_items_query -- Received a disco#items Iq query request. Attributes: stanza -- A reference to the module containing the stanza classes provided by this plugin. static -- Object containing the default set of static node handlers. default_handlers -- A dictionary mapping operations to the default global handler (by default, the static handlers). xmpp -- The main Slixmpp object. Methods: set_node_handler -- Assign a handler to a JID/node combination. del_node_handler -- Remove a handler from a JID/node combination. get_info -- Retrieve disco#info data, locally or remote. get_items -- Retrieve disco#items data, locally or remote. set_identities -- set_features -- set_items -- del_items -- del_identity -- del_feature -- del_item -- add_identity -- add_feature -- add_item -- """ name = 'xep_0030' description = 'XEP-0030: Service Discovery' dependencies = set() stanza = stanza default_config = { 'use_cache': True, 'wrap_results': False } def plugin_init(self): """ Start the XEP-0030 plugin. """ self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), self._handle_disco_info)) self.xmpp.register_handler( Callback('Disco Items', StanzaPath('iq/disco_items'), self._handle_disco_items)) register_stanza_plugin(Iq, DiscoInfo) register_stanza_plugin(Iq, DiscoItems) self.static = StaticDisco(self.xmpp, self) self._disco_ops = [ 'get_info', 'set_info', 'set_identities', 'set_features', 'get_items', 'set_items', 'del_items', 'add_identity', 'del_identity', 'add_feature', 'del_feature', 'add_item', 'del_item', 'del_identities', 'del_features', 'cache_info', 'get_cached_info', 'supports', 'has_identity'] for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) def session_bind(self, jid): self.add_feature('http://jabber.org/protocol/disco#info') def plugin_end(self): self.del_feature('http://jabber.org/protocol/disco#info') def _add_disco_op(self, op, default_handler): self.api.register(default_handler, op) self.api.register_default(default_handler, op) def set_node_handler(self, htype, jid=None, node=None, handler=None): """ Add a node handler for the given hierarchy level and handler type. Node handlers are ordered in a hierarchy where the most specific handler is executed. Thus, a fallback, global handler can be used for the majority of cases with a few node specific handler that override the global behavior. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Handler types: get_info get_items set_identities set_features set_items del_items del_identities del_identity del_feature del_features del_item add_identity add_feature add_item Arguments: htype -- The operation provided by the handler. jid -- The JID the handler applies to. May be narrowed further if a node is given. node -- The particular node the handler is for. If no JID is given, then the self.xmpp.boundjid.full is assumed. handler -- The handler function to use. """ self.api.register(handler, htype, jid, node) def del_node_handler(self, htype, jid, node): """ Remove a handler type for a JID and node combination. The next handler in the hierarchy will be used if one exists. If removing the global handler, make sure that other handlers exist to process existing nodes. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Arguments: htype -- The type of handler to remove. jid -- The JID from which to remove the handler. node -- The node from which to remove the handler. """ self.api.unregister(htype, jid, node) def restore_defaults(self, jid=None, node=None, handlers=None): """ Change all or some of a node's handlers to the default handlers. Useful for manually overriding the contents of a node that would otherwise be handled by a JID level or global level dynamic handler. The default is to use the built-in static handlers, but that may be changed by modifying self.default_handlers. Arguments: jid -- The JID owning the node to modify. node -- The node to change to using static handlers. handlers -- Optional list of handlers to change to the default version. If provided, only these handlers will be changed. Otherwise, all handlers will use the default version. """ if handlers is None: handlers = self._disco_ops for op in handlers: self.api.restore_default(op, jid, node) def supports(self, jid=None, node=None, feature=None, local=False, cached=True, ifrom=None): """ Check if a JID supports a given feature. Return values: True -- The feature is supported False -- The feature is not listed as supported None -- Nothing could be found due to a timeout Arguments: jid -- Request info from this JID. node -- The particular node to query. feature -- The name of the feature to check. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. ifrom -- Specifiy the sender's JID. """ data = {'feature': feature, 'local': local, 'cached': cached} return self.api['supports'](jid, node, ifrom, data) def has_identity(self, jid=None, node=None, category=None, itype=None, lang=None, local=False, cached=True, ifrom=None): """ Check if a JID provides a given identity. Return values: True -- The identity is provided False -- The identity is not listed None -- Nothing could be found due to a timeout Arguments: jid -- Request info from this JID. node -- The particular node to query. category -- The category of the identity to check. itype -- The type of the identity to check. lang -- The language of the identity to check. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. ifrom -- Specifiy the sender's JID. """ data = {'category': category, 'itype': itype, 'lang': lang, 'local': local, 'cached': cached} return self.api['has_identity'](jid, node, ifrom, data) @future_wrapper def get_info(self, jid=None, node=None, local=None, cached=None, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. Info may be retrieved from both local resources and remote agents; the local parameter indicates if the information should be gathered by executing the local node handlers, or if a disco#info stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoInfo stanza will be returned. Otherwise, an Iq stanza will be returned. Arguments: jid -- Request info from this JID. node -- The particular node to query. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. ifrom -- Specifiy the sender's JID. timeout -- The time in seconds to wait for reply, before calling timeout_callback callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. timeout_callback -- Optional callback to execute when no result has been received in timeout seconds. """ if local is None: if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full elif jid in (None, ''): local = True if local: log.debug("Looking up local disco#info data " + \ "for %s, node %s.", jid, node) info = self.api['get_info'](jid, node, kwargs.get('ifrom', None), kwargs) info = self._fix_default_info(info) return self._wrap(kwargs.get('ifrom', None), jid, info) if cached: log.debug("Looking up cached disco#info data " + \ "for %s, node %s.", jid, node) info = self.api['get_cached_info'](jid, node, kwargs.get('ifrom', None), kwargs) if info is not None: return self._wrap(kwargs.get('ifrom', None), jid, info) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_info']['node'] = node if node else '' return iq.send(timeout=kwargs.get('timeout', None), callback=kwargs.get('callback', None), timeout_callback=kwargs.get('timeout_callback', None)) def set_info(self, jid=None, node=None, info=None): """ Set the disco#info data for a JID/node based on an existing disco#info stanza. """ if isinstance(info, Iq): info = info['disco_info'] self.api['set_info'](jid, node, None, info) @future_wrapper def get_items(self, jid=None, node=None, local=False, **kwargs): """ Retrieve the disco#items results from a given JID/node combination. Items may be retrieved from both local resources and remote agents; the local parameter indicates if the items should be gathered by executing the local node handlers, or if a disco#items stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoItems stanza will be returned. Otherwise, an Iq stanza will be returned. Arguments: jid -- Request info from this JID. node -- The particular node to query. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. ifrom -- Specifiy the sender's JID. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. timeout_callback -- Optional callback to execute when no result has been received in timeout seconds. """ if local or local is None and jid is None: items = self.api['get_items'](jid, node, kwargs.get('ifrom', None), kwargs) return self._wrap(kwargs.get('ifrom', None), jid, items) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_items']['node'] = node if node else '' if kwargs.get('iterator', False) and self.xmpp['xep_0059']: raise NotImplementedError("XEP 0059 has not yet been fixed") return self.xmpp['xep_0059'].iterate(iq, 'disco_items') else: return iq.send(timeout=kwargs.get('timeout', None), callback=kwargs.get('callback', None), timeout_callback=kwargs.get('timeout_callback', None)) def set_items(self, jid=None, node=None, **kwargs): """ Set or replace all items for the specified JID/node combination. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name). Arguments: jid -- The JID to modify. node -- Optional node to modify. items -- A series of items in tuple format. """ self.api['set_items'](jid, node, None, kwargs) def del_items(self, jid=None, node=None, **kwargs): """ Remove all items from the given JID/node combination. Arguments: jid -- The JID to modify. node -- Optional node to modify. """ self.api['del_items'](jid, node, None, kwargs) def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ Add a new item element to the given JID/node combination. Each item is required to have a JID, but may also specify a node value to reference non-addressable entities. Arguments: jid -- The JID for the item. name -- Optional name for the item. node -- The node to modify. subnode -- Optional node for the item. ijid -- The JID to modify. """ if not jid: jid = self.xmpp.boundjid.full kwargs = {'ijid': jid, 'name': name, 'inode': subnode} self.api['add_item'](ijid, node, None, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ Remove a single item from the given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. ijid -- The item's JID. inode -- The item's node. """ self.api['del_item'](jid, node, None, kwargs) def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): """ Add a new identity to the given JID/node combination. Each identity must be unique in terms of all four identity components: category, type, name, and language. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. A category and type is always required. Arguments: category -- The identity's category. itype -- The identity's type. name -- Optional name for the identity. lang -- Optional two-letter language code. node -- The node to modify. jid -- The JID to modify. """ kwargs = {'category': category, 'itype': itype, 'name': name, 'lang': lang} self.api['add_identity'](jid, node, None, kwargs) def add_feature(self, feature, node=None, jid=None): """ Add a feature to a JID/node combination. Arguments: feature -- The namespace of the supported feature. node -- The node to modify. jid -- The JID to modify. """ kwargs = {'feature': feature} self.api['add_feature'](jid, node, None, kwargs) def del_identity(self, jid=None, node=None, **kwargs): """ Remove an identity from the given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. category -- The identity's category. itype -- The identity's type value. name -- Optional, human readable name for the identity. lang -- Optional, the identity's xml:lang value. """ self.api['del_identity'](jid, node, None, kwargs) def del_feature(self, jid=None, node=None, **kwargs): """ Remove a feature from a given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. feature -- The feature's namespace. """ self.api['del_feature'](jid, node, None, kwargs) def set_identities(self, jid=None, node=None, **kwargs): """ Add or replace all identities for the given JID/node combination. The identities must be in a set where each identity is a tuple of the form: (category, type, lang, name) Arguments: jid -- The JID to modify. node -- The node to modify. identities -- A set of identities in tuple form. lang -- Optional, xml:lang value. """ self.api['set_identities'](jid, node, None, kwargs) def del_identities(self, jid=None, node=None, **kwargs): """ Remove all identities for a JID/node combination. If a language is specified, only identities using that language will be removed. Arguments: jid -- The JID to modify. node -- The node to modify. lang -- Optional. If given, only remove identities using this xml:lang value. """ self.api['del_identities'](jid, node, None, kwargs) def set_features(self, jid=None, node=None, **kwargs): """ Add or replace the set of supported features for a JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. features -- The new set of supported features. """ self.api['set_features'](jid, node, None, kwargs) def del_features(self, jid=None, node=None, **kwargs): """ Remove all features from a JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. """ self.api['del_features'](jid, node, None, kwargs) def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None): """ Execute the most specific node handler for the given JID/node combination. Arguments: htype -- The handler type to execute. jid -- The JID requested. node -- The node requested. data -- Optional, custom data to pass to the handler. """ if not data: data = {} return self.api[htype](jid, node, ifrom, data) def _handle_disco_info(self, iq): """ Process an incoming disco#info stanza. If it is a get request, find and return the appropriate identities and features. If it is an info result, fire the disco_info event. Arguments: iq -- The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco info query from " + \ "<%s> to <%s>.", iq['from'], iq['to']) info = self.api['get_info'](iq['to'], iq['disco_info']['node'], iq['from'], iq) if isinstance(info, Iq): info['id'] = iq['id'] info.send() else: iq = iq.reply() if info: info = self._fix_default_info(info) iq.set_payload(info.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco info result from " + \ "<%s> to <%s>.", iq['from'], iq['to']) if self.use_cache: log.debug("Caching disco info result from " \ "<%s> to <%s>.", iq['from'], iq['to']) if self.xmpp.is_component: ito = iq['to'].full else: ito = None self.api['cache_info'](iq['from'], iq['disco_info']['node'], ito, iq) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): """ Process an incoming disco#items stanza. If it is a get request, find and return the appropriate items. If it is an items result, fire the disco_items event. Arguments: iq -- The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco items query from " + \ "<%s> to <%s>.", iq['from'], iq['to']) items = self.api['get_items'](iq['to'], iq['disco_items']['node'], iq['from'], iq) if isinstance(items, Iq): items.send() else: iq = iq.reply() if items: iq.set_payload(items.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco items result from " + \ "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_items', iq) def _fix_default_info(self, info): """ Disco#info results for a JID are required to include at least one identity and feature. As a default, if no other identity is provided, Slixmpp will use either the generic component or the bot client identity. A the standard disco#info feature will also be added if no features are provided. Arguments: info -- The disco#info quest (not the full Iq stanza) to modify. """ result = info if isinstance(info, Iq): info = info['disco_info'] if not info['node']: if not info['identities']: if self.xmpp.is_component: log.debug("No identity found for this entity. " + \ "Using default component identity.") info.add_identity('component', 'generic') else: log.debug("No identity found for this entity. " + \ "Using default client identity.") info.add_identity('client', 'bot') if not info['features']: log.debug("No features found for this entity. " + \ "Using default disco#info feature.") info.add_feature(info.namespace) return result def _wrap(self, ito, ifrom, payload, force=False): """ Ensure that results are wrapped in an Iq stanza if self.wrap_results has been set to True. Arguments: ito -- The JID to use as the 'to' value ifrom -- The JID to use as the 'from' value payload -- The disco data to wrap force -- Force wrapping, regardless of self.wrap_results """ if (force or self.wrap_results) and not isinstance(payload, Iq): iq = self.xmpp.Iq() # Since we're simulating a result, we have to treat # the 'from' and 'to' values opposite the normal way. iq['to'] = self.xmpp.boundjid if ito is None else ito iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom iq['type'] = 'result' iq.append(payload) return iq return payload slixmpp-1.2.2/slixmpp/plugins/xep_0030/stanza/0000755000175000001440000000000013014656513022113 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0030/stanza/__init__.py0000644000175000001440000000046512424504520024223 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo from slixmpp.plugins.xep_0030.stanza.items import DiscoItems slixmpp-1.2.2/slixmpp/plugins/xep_0030/stanza/info.py0000644000175000001440000002424713004224717023425 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class DiscoInfo(ElementBase): """ XMPP allows for users and agents to find the identities and features supported by other entities in the XMPP network through service discovery, or "disco". In particular, the "disco#info" query type for stanzas is used to request the list of identities and features offered by a JID. An identity is a combination of a category and type, such as the 'client' category with a type of 'pc' to indicate the agent is a human operated client with a GUI, or a category of 'gateway' with a type of 'aim' to identify the agent as a gateway for the legacy AIM protocol. See for a full list of accepted category and type combinations. Features are simply a set of the namespaces that identify the supported features. For example, a client that supports service discovery will include the feature 'http://jabber.org/protocol/disco#info'. Since clients and components may operate in several roles at once, identity and feature information may be grouped into "nodes". If one were to write all of the identities and features used by a client, then node names would be like section headings. Example disco#info stanzas: Stanza Interface: node -- The name of the node to either query or return info from. identities -- A set of 4-tuples, where each tuple contains the category, type, xml:lang, and name of an identity. features -- A set of namespaces for features. Methods: add_identity -- Add a new, single identity. del_identity -- Remove a single identity. get_identities -- Return all identities in tuple form. set_identities -- Use multiple identities, each given in tuple form. del_identities -- Remove all identities. add_feature -- Add a single feature. del_feature -- Remove a single feature. get_features -- Return a list of all features. set_features -- Use a given list of features. del_features -- Remove all features. """ name = 'query' namespace = 'http://jabber.org/protocol/disco#info' plugin_attrib = 'disco_info' interfaces = {'node', 'features', 'identities'} lang_interfaces = {'identities'} # Cache identities and features _identities = set() _features = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches identity and feature information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._identities = {id[0:3] for id in self['identities']} self._features = self['features'] def add_identity(self, category, itype, name=None, lang=None): """ Add a new identity element. Each identity must be unique in terms of all four identity components. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. In any case, a category and type are required. Arguments: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ identity = (category, itype, lang) if identity not in self._identities: self._identities.add(identity) id_xml = ET.Element('{%s}identity' % self.namespace) id_xml.attrib['category'] = category id_xml.attrib['type'] = itype if lang: id_xml.attrib['{%s}lang' % self.xml_ns] = lang if name: id_xml.attrib['name'] = name self.xml.insert(0, id_xml) return True return False def del_identity(self, category, itype, name=None, lang=None): """ Remove a given identity. Arguments: category -- The general category to which the agent belonged. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ identity = (category, itype, lang) if identity in self._identities: self._identities.remove(identity) for id_xml in self.xml.findall('{%s}identity' % self.namespace): id = (id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None)) if id == identity: self.xml.remove(id_xml) return True return False def get_identities(self, lang=None, dedupe=True): """ Return a set of all identities in tuple form as so: (category, type, lang, name) If a language was specified, only return identities using that language. Arguments: lang -- Optional, standard xml:lang value. dedupe -- If True, de-duplicate identities, otherwise return a list of all identities. """ if dedupe: identities = set() else: identities = [] for id_xml in self.xml.findall('{%s}identity' % self.namespace): xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) if lang is None or xml_lang == lang: id = (id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None), id_xml.attrib.get('name', None)) if dedupe: identities.add(id) else: identities.append(id) return identities def set_identities(self, identities, lang=None): """ Add or replace all identities. The identities must be a in set where each identity is a tuple of the form: (category, type, lang, name) If a language is specifified, any identities using that language will be removed to be replaced with the given identities. NOTE: An identity's language will not be changed regardless of the value of lang. Arguments: identities -- A set of identities in tuple form. lang -- Optional, standard xml:lang value. """ self.del_identities(lang) for identity in identities: category, itype, lang, name = identity self.add_identity(category, itype, name, lang) def del_identities(self, lang=None): """ Remove all identities. If a language was specified, only remove identities using that language. Arguments: lang -- Optional, standard xml:lang value. """ for id_xml in self.xml.findall('{%s}identity' % self.namespace): if lang is None: self.xml.remove(id_xml) elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang: self._identities.remove(( id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None))) self.xml.remove(id_xml) def add_feature(self, feature): """ Add a single, new feature. Arguments: feature -- The namespace of the supported feature. """ if feature not in self._features: self._features.add(feature) feature_xml = ET.Element('{%s}feature' % self.namespace) feature_xml.attrib['var'] = feature self.xml.append(feature_xml) return True return False def del_feature(self, feature): """ Remove a single feature. Arguments: feature -- The namespace of the removed feature. """ if feature in self._features: self._features.remove(feature) for feature_xml in self.xml.findall('{%s}feature' % self.namespace): if feature_xml.attrib['var'] == feature: self.xml.remove(feature_xml) return True return False def get_features(self, dedupe=True): """Return the set of all supported features.""" if dedupe: features = set() else: features = [] for feature_xml in self.xml.findall('{%s}feature' % self.namespace): if dedupe: features.add(feature_xml.attrib['var']) else: features.append(feature_xml.attrib['var']) return features def set_features(self, features): """ Add or replace the set of supported features. Arguments: features -- The new set of supported features. """ self.del_features() for feature in features: self.add_feature(feature) def del_features(self): """Remove all features.""" self._features = set() for feature_xml in self.xml.findall('{%s}feature' % self.namespace): self.xml.remove(feature_xml) slixmpp-1.2.2/slixmpp/plugins/xep_0030/stanza/items.py0000644000175000001440000001076713004224717023615 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, register_stanza_plugin class DiscoItems(ElementBase): """ Example disco#items stanzas: Stanza Interface: node -- The name of the node to either query or return info from. items -- A list of 3-tuples, where each tuple contains the JID, node, and name of an item. Methods: add_item -- Add a single new item. del_item -- Remove a single item. get_items -- Return all items. set_items -- Set or replace all items. del_items -- Remove all items. """ name = 'query' namespace = 'http://jabber.org/protocol/disco#items' plugin_attrib = 'disco_items' interfaces = {'node', 'items'} # Cache items _items = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._items = {item[0:2] for item in self['items']} def add_item(self, jid, node=None, name=None): """ Add a new item element. Each item is required to have a JID, but may also specify a node value to reference non-addressable entitities. Arguments: jid -- The JID for the item. node -- Optional additional information to reference non-addressable items. name -- Optional human readable name for the item. """ if (jid, node) not in self._items: self._items.add((jid, node)) item = DiscoItem(parent=self) item['jid'] = jid item['node'] = node item['name'] = name self.iterables.append(item) return True return False def del_item(self, jid, node=None): """ Remove a single item. Arguments: jid -- JID of the item to remove. node -- Optional extra identifying information. """ if (jid, node) in self._items: for item_xml in self.xml.findall('{%s}item' % self.namespace): item = (item_xml.attrib['jid'], item_xml.attrib.get('node', None)) if item == (jid, node): self.xml.remove(item_xml) return True return False def get_items(self): """Return all items.""" items = set() for item in self['substanzas']: if isinstance(item, DiscoItem): items.add((item['jid'], item['node'], item['name'])) return items def set_items(self, items): """ Set or replace all items. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name) Arguments: items -- A series of items in tuple format. """ self.del_items() for item in items: jid, node, name = item self.add_item(jid, node, name) def del_items(self): """Remove all items.""" self._items = set() items = [i for i in self.iterables if isinstance(i, DiscoItem)] for item in items: self.xml.remove(item.xml) self.iterables.remove(item) class DiscoItem(ElementBase): name = 'item' namespace = 'http://jabber.org/protocol/disco#items' plugin_attrib = name interfaces = {'jid', 'node', 'name'} def get_node(self): """Return the item's node name or ``None``.""" return self._get_attr('node', None) def get_name(self): """Return the item's human readable name, or ``None``.""" return self._get_attr('name', None) register_stanza_plugin(DiscoItems, DiscoItem, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0030/static.py0000644000175000001440000003446212770302340022457 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.xmlstream import JID from slixmpp.plugins.xep_0030 import DiscoInfo, DiscoItems log = logging.getLogger(__name__) class StaticDisco(object): """ While components will likely require fully dynamic handling of service discovery information, most clients and simple bots only need to manage a few disco nodes that will remain mostly static. StaticDisco provides a set of node handlers that will store static sets of disco info and items in memory. Attributes: nodes -- A dictionary mapping (JID, node) tuples to a dict containing a disco#info and a disco#items stanza. xmpp -- The main Slixmpp object. """ def __init__(self, xmpp, disco): """ Create a static disco interface. Sets of disco#info and disco#items are maintained for every given JID and node combination. These stanzas are used to store disco information in memory without any additional processing. Arguments: xmpp -- The main Slixmpp object. """ self.nodes = {} self.xmpp = xmpp self.disco = disco def add_node(self, jid=None, node=None, ifrom=None): """ Create a new set of stanzas for the provided JID and node combination. Arguments: jid -- The JID that will own the new stanzas. node -- The node that will own the new stanzas. """ if jid is None: jid = self.xmpp.boundjid.full if node is None: node = '' if ifrom is None: ifrom = '' if isinstance(ifrom, JID): ifrom = ifrom.full if (jid, node, ifrom) not in self.nodes: self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(), 'items': DiscoItems()} self.nodes[(jid, node, ifrom)]['info']['node'] = node self.nodes[(jid, node, ifrom)]['items']['node'] = node def get_node(self, jid=None, node=None, ifrom=None): if jid is None: jid = self.xmpp.boundjid.full if node is None: node = '' if ifrom is None: ifrom = '' if isinstance(ifrom, JID): ifrom = ifrom.full if (jid, node, ifrom) not in self.nodes: self.add_node(jid, node, ifrom) return self.nodes[(jid, node, ifrom)] def node_exists(self, jid=None, node=None, ifrom=None): if jid is None: jid = self.xmpp.boundjid.full if node is None: node = '' if ifrom is None: ifrom = '' if isinstance(ifrom, JID): ifrom = ifrom.full if (jid, node, ifrom) not in self.nodes: return False return True # ================================================================= # Node Handlers # # Each handler accepts four arguments: jid, node, ifrom, and data. # The jid and node parameters together determine the set of info # and items stanzas that will be retrieved or added. Additionally, # the ifrom value allows for cached results when results vary based # on the requester's JID. The data parameter is a dictionary with # additional parameters that will be passed to other calls. # # This implementation does not allow different responses based on # the requester's JID, except for cached results. To do that, # register a custom node handler. def supports(self, jid, node, ifrom, data): """ Check if a JID supports a given feature. The data parameter may provide: feature -- The feature to check for support. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ feature = data.get('feature', None) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} if not feature: return False try: info = self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) features = info['disco_info']['features'] return feature in features except IqError: return False except IqTimeout: return None def has_identity(self, jid, node, ifrom, data): """ Check if a JID has a given identity. The data parameter may provide: category -- The category of the identity to check. itype -- The type of the identity to check. lang -- The language of the identity to check. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ identity = (data.get('category', None), data.get('itype', None), data.get('lang', None)) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} try: info = self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) trunc = lambda i: (i[0], i[1], i[2]) return identity in map(trunc, info['disco_info']['identities']) except IqError: return False except IqTimeout: return None def get_info(self, jid, node, ifrom, data): """ Return the stored info data for the requested JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): if not node: return DiscoInfo() else: raise XMPPError(condition='item-not-found') else: return self.get_node(jid, node)['info'] def set_info(self, jid, node, ifrom, data): """ Set the entire info stanza for a JID/node at once. The data parameter is a disco#info substanza. """ self.add_node(jid, node) self.get_node(jid, node)['info'] = data def del_info(self, jid, node, ifrom, data): """ Reset the info stanza for a given JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'] = DiscoInfo() def get_items(self, jid, node, ifrom, data): """ Return the stored items data for the requested JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): if not node: return DiscoItems() else: raise XMPPError(condition='item-not-found') else: return self.get_node(jid, node)['items'] def set_items(self, jid, node, ifrom, data): """ Replace the stored items data for a JID/node combination. The data parameter may provide: items -- A set of items in tuple format. """ items = data.get('items', set()) self.add_node(jid, node) self.get_node(jid, node)['items']['items'] = items def del_items(self, jid, node, ifrom, data): """ Reset the items stanza for a given JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): self.get_node(jid, node)['items'] = DiscoItems() def add_identity(self, jid, node, ifrom, data): """ Add a new identity to te JID/node combination. The data parameter may provide: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ self.add_node(jid, node) self.get_node(jid, node)['info'].add_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def set_identities(self, jid, node, ifrom, data): """ Add or replace all identities for a JID/node combination. The data parameter should include: identities -- A list of identities in tuple form: (category, type, name, lang) """ identities = data.get('identities', set()) self.add_node(jid, node) self.get_node(jid, node)['info']['identities'] = identities def del_identity(self, jid, node, ifrom, data): """ Remove an identity from a JID/node combination. The data parameter may provide: category -- The general category to which the agent belonged. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'].del_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def del_identities(self, jid, node, ifrom, data): """ Remove all identities from a JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): del self.get_node(jid, node)['info']['identities'] def add_feature(self, jid, node, ifrom, data): """ Add a feature to a JID/node combination. The data parameter should include: feature -- The namespace of the supported feature. """ self.add_node(jid, node) self.get_node(jid, node)['info'].add_feature( data.get('feature', '')) def set_features(self, jid, node, ifrom, data): """ Add or replace all features for a JID/node combination. The data parameter should include: features -- The new set of supported features. """ features = data.get('features', set()) self.add_node(jid, node) self.get_node(jid, node)['info']['features'] = features def del_feature(self, jid, node, ifrom, data): """ Remove a feature from a JID/node combination. The data parameter should include: feature -- The namespace of the removed feature. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'].del_feature( data.get('feature', '')) def del_features(self, jid, node, ifrom, data): """ Remove all features from a JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): return del self.get_node(jid, node)['info']['features'] def add_item(self, jid, node, ifrom, data): """ Add an item to a JID/node combination. The data parameter may include: ijid -- The JID for the item. inode -- Optional additional information to reference non-addressable items. name -- Optional human readable name for the item. """ self.add_node(jid, node) self.get_node(jid, node)['items'].add_item( data.get('ijid', ''), node=data.get('inode', ''), name=data.get('name', '')) def del_item(self, jid, node, ifrom, data): """ Remove an item from a JID/node combination. The data parameter may include: ijid -- JID of the item to remove. inode -- Optional extra identifying information. """ if self.node_exists(jid, node): self.get_node(jid, node)['items'].del_item( data.get('ijid', ''), node=data.get('inode', None)) def cache_info(self, jid, node, ifrom, data): """ Cache disco information for an external JID. The data parameter is the Iq result stanza containing the disco info to cache, or the disco#info substanza itself. """ if isinstance(data, Iq): data = data['disco_info'] self.add_node(jid, node, ifrom) self.get_node(jid, node, ifrom)['info'] = data def get_cached_info(self, jid, node, ifrom, data): """ Retrieve cached disco info data. The data parameter is not used. """ if not self.node_exists(jid, node, ifrom): return None else: return self.get_node(jid, node, ifrom)['info'] slixmpp-1.2.2/slixmpp/plugins/xep_0333/0000755000175000001440000000000013014656513020621 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0333/__init__.py0000644000175000001440000000060512760133734022735 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Copyright (C) 2016 Emmanuel Gil Peyrot This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0333.stanza import Markable, Received, Displayed, Acknowledged from slixmpp.plugins.xep_0333.hints import XEP_0333 register_plugin(XEP_0333) slixmpp-1.2.2/slixmpp/plugins/xep_0333/hints.py0000644000175000001440000000343413004225345022316 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Copyright (C) 2016 Emmanuel Gil Peyrot This file is part of slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0333 import stanza, Markable, Received, Displayed, Acknowledged log = logging.getLogger(__name__) class XEP_0333(BasePlugin): name = 'xep_0333' description = 'XEP-0333: Chat Markers' stanza = stanza def plugin_init(self): register_stanza_plugin(Message, Markable) register_stanza_plugin(Message, Received) register_stanza_plugin(Message, Displayed) register_stanza_plugin(Message, Acknowledged) self.xmpp.register_handler( Callback('Received Chat Marker', StanzaPath('message/received'), self._handle_received)) self.xmpp.register_handler( Callback('Displayed Chat Marker', StanzaPath('message/displayed'), self._handle_displayed)) self.xmpp.register_handler( Callback('Acknowledged Chat Marker', StanzaPath('message/acknowledged'), self._handle_acknowledged)) def _handle_received(self, message): self.xmpp.event('marker_received', message) self.xmpp.event('marker', message) def _handle_displayed(self, message): self.xmpp.event('marker_displayed', message) self.xmpp.event('marker', message) def _handle_acknowledged(self, message): self.xmpp.event('marker_acknowledged', message) self.xmpp.event('marker', message) slixmpp-1.2.2/slixmpp/plugins/xep_0333/stanza.py0000644000175000001440000000144112760133734022475 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Copyright (C) 2016 Emmanuel Gil Peyrot This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Markable(ElementBase): name = 'markable' plugin_attrib = 'markable' namespace = 'urn:xmpp:chat-markers:0' class Received(ElementBase): name = 'received' plugin_attrib = 'received' namespace = 'urn:xmpp:chat-markers:0' interfaces = {'id'} class Displayed(ElementBase): name = 'displayed' plugin_attrib = 'displayed' namespace = 'urn:xmpp:chat-markers:0' interfaces = {'id'} class Acknowledged(ElementBase): name = 'acknowledged' plugin_attrib = 'acknowledged' namespace = 'urn:xmpp:chat-markers:0' interfaces = {'id'} slixmpp-1.2.2/slixmpp/plugins/xep_0280/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0280/__init__.py0000644000175000001440000000101012424504520022715 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0280.stanza import ReceivedCarbon, SentCarbon from slixmpp.plugins.xep_0280.stanza import PrivateCarbon from slixmpp.plugins.xep_0280.stanza import CarbonEnable, CarbonDisable from slixmpp.plugins.xep_0280.carbons import XEP_0280 register_plugin(XEP_0280) slixmpp-1.2.2/slixmpp/plugins/xep_0280/carbons.py0000644000175000001440000000542313004224717022623 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging import slixmpp from slixmpp.stanza import Message, Iq from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0280 import stanza log = logging.getLogger(__name__) class XEP_0280(BasePlugin): """ XEP-0280 Message Carbons """ name = 'xep_0280' description = 'XEP-0280: Message Carbons' dependencies = {'xep_0030', 'xep_0297'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Carbon Received', StanzaPath('message/carbon_received'), self._handle_carbon_received)) self.xmpp.register_handler( Callback('Carbon Sent', StanzaPath('message/carbon_sent'), self._handle_carbon_sent)) register_stanza_plugin(Message, stanza.ReceivedCarbon) register_stanza_plugin(Message, stanza.SentCarbon) register_stanza_plugin(Message, stanza.PrivateCarbon) register_stanza_plugin(Iq, stanza.CarbonEnable) register_stanza_plugin(Iq, stanza.CarbonDisable) register_stanza_plugin(stanza.ReceivedCarbon, self.xmpp['xep_0297'].stanza.Forwarded) register_stanza_plugin(stanza.SentCarbon, self.xmpp['xep_0297'].stanza.Forwarded) def plugin_end(self): self.xmpp.remove_handler('Carbon Received') self.xmpp.remove_handler('Carbon Sent') self.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:carbons:2') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2') def _handle_carbon_received(self, msg): self.xmpp.event('carbon_received', msg) def _handle_carbon_sent(self, msg): self.xmpp.event('carbon_sent', msg) def enable(self, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('carbon_enable') return iq.send(timeout_callback=timeout_callback, timeout=timeout, callback=callback) def disable(self, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('carbon_disable') return iq.send(timeout_callback=timeout_callback, timeout=timeout, callback=callback) slixmpp-1.2.2/slixmpp/plugins/xep_0280/stanza.py0000644000175000001440000000300213004224717022463 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.xmlstream import ElementBase class ReceivedCarbon(ElementBase): name = 'received' namespace = 'urn:xmpp:carbons:2' plugin_attrib = 'carbon_received' interfaces = {'carbon_received'} is_extension = True def get_carbon_received(self): return self['forwarded']['stanza'] def del_carbon_received(self): del self['forwarded']['stanza'] def set_carbon_received(self, stanza): self['forwarded']['stanza'] = stanza class SentCarbon(ElementBase): name = 'sent' namespace = 'urn:xmpp:carbons:2' plugin_attrib = 'carbon_sent' interfaces = {'carbon_sent'} is_extension = True def get_carbon_sent(self): return self['forwarded']['stanza'] def del_carbon_sent(self): del self['forwarded']['stanza'] def set_carbon_sent(self, stanza): self['forwarded']['stanza'] = stanza class PrivateCarbon(ElementBase): name = 'private' namespace = 'urn:xmpp:carbons:2' plugin_attrib = 'carbon_private' interfaces = set() class CarbonEnable(ElementBase): name = 'enable' namespace = 'urn:xmpp:carbons:2' plugin_attrib = 'carbon_enable' interfaces = set() class CarbonDisable(ElementBase): name = 'disable' namespace = 'urn:xmpp:carbons:2' plugin_attrib = 'carbon_disable' interfaces = set() slixmpp-1.2.2/slixmpp/plugins/xep_0095/0000755000175000001440000000000013014656513020626 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0095/__init__.py0000644000175000001440000000064612424504520022737 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0095 import stanza from slixmpp.plugins.xep_0095.stanza import SI from slixmpp.plugins.xep_0095.stream_initiation import XEP_0095 register_plugin(XEP_0095) slixmpp-1.2.2/slixmpp/plugins/xep_0095/stream_initiation.py0000644000175000001440000001551513004224717024725 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import threading from uuid import uuid4 from slixmpp import Iq, Message from slixmpp.exceptions import XMPPError from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0095 import stanza, SI log = logging.getLogger(__name__) SOCKS5 = 'http://jabber.org/protocol/bytestreams' IBB = 'http://jabber.org/protocol/ibb' class XEP_0095(BasePlugin): name = 'xep_0095' description = 'XEP-0095: Stream Initiation' dependencies = {'xep_0020', 'xep_0030', 'xep_0047', 'xep_0065'} stanza = stanza def plugin_init(self): self._profiles = {} self._methods = {} self._methods_order = [] self._pending_lock = threading.Lock() self._pending= {} self.register_method(SOCKS5, 'xep_0065', 100) self.register_method(IBB, 'xep_0047', 50) register_stanza_plugin(Iq, SI) register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation) self.xmpp.register_handler( Callback('SI Request', StanzaPath('iq@type=set/si'), self._handle_request)) self.api.register(self._add_pending, 'add_pending', default=True) self.api.register(self._get_pending, 'get_pending', default=True) self.api.register(self._del_pending, 'del_pending', default=True) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(SI.namespace) def plugin_end(self): self.xmpp.remove_handler('SI Request') self.xmpp['xep_0030'].del_feature(feature=SI.namespace) def register_profile(self, profile_name, plugin): self._profiles[profile_name] = plugin def unregister_profile(self, profile_name): try: del self._profiles[profile_name] except KeyError: pass def register_method(self, method, plugin_name, order=50): self._methods[method] = (plugin_name, order) self._methods_order.append((order, method, plugin_name)) self._methods_order.sort() def unregister_method(self, method): if method in self._methods: plugin_name, order = self._methods[method] del self._methods[method] self._methods_order.remove((order, method, plugin_name)) self._methods_order.sort() def _handle_request(self, iq): profile = iq['si']['profile'] sid = iq['si']['id'] if not sid: raise XMPPError(etype='modify', condition='bad-request') if profile not in self._profiles: raise XMPPError( etype='modify', condition='bad-request', extension='bad-profile', extension_ns=SI.namespace) neg = iq['si']['feature_neg']['form']['fields'] options = neg['stream-method']['options'] or [] methods = [] for opt in options: methods.append(opt['value']) for method in methods: if method in self._methods: supported = True break else: raise XMPPError('bad-request', extension='no-valid-streams', extension_ns=SI.namespace) selected_method = None log.debug('Available: %s', methods) for order, method, plugin in self._methods_order: log.debug('Testing: %s', method) if method in methods: selected_method = method break receiver = iq['to'] sender = iq['from'] self.api['add_pending'](receiver, sid, sender, { 'response_id': iq['id'], 'method': selected_method, 'profile': profile }) self.xmpp.event('si_request', iq) def offer(self, jid, sid=None, mime_type=None, profile=None, methods=None, payload=None, ifrom=None, **iqargs): if sid is None: sid = uuid4().hex if methods is None: methods = list(self._methods.keys()) if not isinstance(methods, (list, tuple, set)): methods = [methods] si = self.xmpp.Iq() si['to'] = jid si['from'] = ifrom si['type'] = 'set' si['si']['id'] = sid si['si']['mime_type'] = mime_type si['si']['profile'] = profile if not isinstance(payload, (list, tuple, set)): payload = [payload] for item in payload: si['si'].append(item) si['si']['feature_neg']['form'].add_field( var='stream-method', ftype='list-single', options=methods) return si.send(**iqargs) def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None): stream = self.api['get_pending'](ifrom, sid, jid) iq = self.xmpp.Iq() iq['id'] = stream['response_id'] iq['to'] = jid iq['from'] = ifrom iq['type'] = 'result' if payload: iq['si'].append(payload) iq['si']['feature_neg']['form']['type'] = 'submit' iq['si']['feature_neg']['form'].add_field( var='stream-method', ftype='list-single', value=stream['method']) if ifrom is None: ifrom = self.xmpp.boundjid method_plugin = self._methods[stream['method']][0] self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid) self.api['del_pending'](ifrom, sid, jid) if stream_handler: self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid), stream_handler, disposable=True) return iq.send() def decline(self, jid, sid, ifrom=None): stream = self.api['get_pending'](ifrom, sid, jid) if not stream: return iq = self.xmpp.Iq() iq['id'] = stream['response_id'] iq['to'] = jid iq['from'] = ifrom iq['type'] = 'error' iq['error']['condition'] = 'forbidden' iq['error']['text'] = 'Offer declined' self.api['del_pending'](ifrom, sid, jid) return iq.send() def _add_pending(self, jid, node, ifrom, data): with self._pending_lock: self._pending[(jid, node, ifrom)] = data def _get_pending(self, jid, node, ifrom, data): with self._pending_lock: return self._pending.get((jid, node, ifrom), None) def _del_pending(self, jid, node, ifrom, data): with self._pending_lock: if (jid, node, ifrom) in self._pending: del self._pending[(jid, node, ifrom)] slixmpp-1.2.2/slixmpp/plugins/xep_0095/stanza.py0000644000175000001440000000120013004224717022465 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class SI(ElementBase): name = 'si' namespace = 'http://jabber.org/protocol/si' plugin_attrib = 'si' interfaces = {'id', 'mime_type', 'profile'} def get_mime_type(self): return self._get_attr('mime-type', 'application/octet-stream') def set_mime_type(self, value): self._set_attr('mime-type', value) def del_mime_type(self): self._del_attr('mime-type') slixmpp-1.2.2/slixmpp/plugins/xep_0091/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0091/__init__.py0000644000175000001440000000065212424504520022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0091 import stanza from slixmpp.plugins.xep_0091.stanza import LegacyDelay from slixmpp.plugins.xep_0091.legacy_delay import XEP_0091 register_plugin(XEP_0091) slixmpp-1.2.2/slixmpp/plugins/xep_0091/legacy_delay.py0000644000175000001440000000132712424504520023613 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0091 import stanza class XEP_0091(BasePlugin): """ XEP-0091: Legacy Delayed Delivery """ name = 'xep_0091' description = 'XEP-0091: Legacy Delayed Delivery' dependencies = set() stanza = stanza def plugin_init(self): register_stanza_plugin(Message, stanza.LegacyDelay) register_stanza_plugin(Presence, stanza.LegacyDelay) slixmpp-1.2.2/slixmpp/plugins/xep_0091/stanza.py0000644000175000001440000000230113004224717022464 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class LegacyDelay(ElementBase): name = 'x' namespace = 'jabber:x:delay' plugin_attrib = 'legacy_delay' interfaces = {'from', 'stamp', 'text'} def get_from(self): from_ = self._get_attr('from') return JID(from_) if from_ else None def set_from(self, value): self._set_attr('from', str(value)) def get_stamp(self): timestamp = self._get_attr('stamp') return xep_0082.parse('%sZ' % timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): value = value.astimezone(xep_0082.tzutc) value = xep_0082.format_datetime(value) self._set_attr('stamp', value[0:19].replace('-', '')) def get_text(self): return self.xml.text def set_text(self, value): self.xml.text = value def del_text(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/plugins/xep_0066/0000755000175000001440000000000013014656513020624 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0066/__init__.py0000644000175000001440000000064612770302340022735 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0066 import stanza from slixmpp.plugins.xep_0066.stanza import OOB, OOBTransfer from slixmpp.plugins.xep_0066.oob import XEP_0066 register_plugin(XEP_0066) slixmpp-1.2.2/slixmpp/plugins/xep_0066/oob.py0000644000175000001440000001260313004224717021753 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import Message, Presence, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0066 import stanza log = logging.getLogger(__name__) class XEP_0066(BasePlugin): """ XEP-0066: Out of Band Data Out of Band Data is a basic method for transferring files between XMPP agents. The URL of the resource in question is sent to the receiving entity, which then downloads the resource before responding to the OOB request. OOB is also used as a generic means to transmit URLs in other stanzas to indicate where to find additional information. Also see . Events: oob_transfer -- Raised when a request to download a resource has been received. Methods: send_oob -- Send a request to another entity to download a file or other addressable resource. """ name = 'xep_0066' description = 'XEP-0066: Out of Band Data' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): """Start the XEP-0066 plugin.""" self.url_handlers = {'global': self._default_handler, 'jid': {}} register_stanza_plugin(Iq, stanza.OOBTransfer) register_stanza_plugin(Message, stanza.OOB) register_stanza_plugin(Presence, stanza.OOB) self.xmpp.register_handler( Callback('OOB Transfer', StanzaPath('iq@type=set/oob_transfer'), self._handle_transfer)) def plugin_end(self): self.xmpp.remove_handler('OOB Transfer') self.xmpp['xep_0030'].del_feature(feature=stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].del_feature(feature=stanza.OOB.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) def register_url_handler(self, jid=None, handler=None): """ Register a handler to process download requests, either for all JIDs or a single JID. Arguments: jid -- If None, then set the handler as a global default. handler -- If None, then remove the existing handler for the given JID, or reset the global handler if the JID is None. """ if jid is None: if handler is not None: self.url_handlers['global'] = handler else: self.url_handlers['global'] = self._default_handler else: if handler is not None: self.url_handlers['jid'][jid] = handler else: del self.url_handlers['jid'][jid] def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): """ Initiate a basic file transfer by sending the URL of a file or other resource. Arguments: url -- The URL of the resource to transfer. desc -- An optional human readable description of the item that is to be transferred. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = to iq['from'] = ifrom iq['oob_transfer']['url'] = url iq['oob_transfer']['desc'] = desc return iq.send(**iqargs) def _run_url_handler(self, iq): """ Execute the appropriate handler for a transfer request. Arguments: iq -- The Iq stanza containing the OOB transfer request. """ if iq['to'] in self.url_handlers['jid']: return self.url_handlers['jid'][iq['to']](iq) else: if self.url_handlers['global']: self.url_handlers['global'](iq) else: raise XMPPError('service-unavailable') def _default_handler(self, iq): """ As a safe default, don't actually download files. Register a new handler using self.register_url_handler to screen requests and download files. Arguments: iq -- The Iq stanza containing the OOB transfer request. """ raise XMPPError('service-unavailable') def _handle_transfer(self, iq): """ Handle receiving an out-of-band transfer request. Arguments: iq -- An Iq stanza containing an OOB transfer request. """ log.debug('Received out-of-band data request for %s from %s:' % ( iq['oob_transfer']['url'], iq['from'])) self._run_url_handler(iq) iq.reply().send() slixmpp-1.2.2/slixmpp/plugins/xep_0066/stanza.py0000644000175000001440000000116313004224717022473 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class OOBTransfer(ElementBase): """ """ name = 'query' namespace = 'jabber:iq:oob' plugin_attrib = 'oob_transfer' interfaces = {'url', 'desc', 'sid'} sub_interfaces = {'url', 'desc'} class OOB(ElementBase): """ """ name = 'x' namespace = 'jabber:x:oob' plugin_attrib = 'oob' interfaces = {'url', 'desc'} sub_interfaces = interfaces slixmpp-1.2.2/slixmpp/plugins/__init__.py0000644000175000001440000000627112603534742021476 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin from slixmpp.plugins.base import register_plugin, load_plugin __all__ = [ # XEPS 'xep_0004', # Data Forms 'xep_0009', # Jabber-RPC 'xep_0012', # Last Activity 'xep_0013', # Flexible Offline Message Retrieval 'xep_0016', # Privacy Lists 'xep_0020', # Feature Negotiation 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery 'xep_0033', # Extended Stanza Addresses 'xep_0045', # Multi-User Chat (Client) 'xep_0047', # In-Band Bytestreams 'xep_0048', # Bookmarks 'xep_0049', # Private XML Storage 'xep_0050', # Ad-hoc Commands 'xep_0054', # vcard-temp 'xep_0059', # Result Set Management 'xep_0060', # Pubsub (Client) 'xep_0065', # SOCKS5 Bytestreams 'xep_0066', # Out of Band Data 'xep_0071', # XHTML-IM 'xep_0077', # In-Band Registration # 'xep_0078', # Non-SASL auth. Don't automatically load 'xep_0079', # Advanced Message Processing 'xep_0080', # User Location 'xep_0082', # XMPP Date and Time Profiles 'xep_0084', # User Avatar 'xep_0085', # Chat State Notifications 'xep_0086', # Legacy Error Codes 'xep_0091', # Legacy Delayed Delivery 'xep_0092', # Software Version 'xep_0106', # JID Escaping 'xep_0107', # User Mood 'xep_0108', # User Activity 'xep_0115', # Entity Capabilities 'xep_0118', # User Tune 'xep_0122', # Data Forms Validation 'xep_0128', # Extended Service Discovery 'xep_0131', # Standard Headers and Internet Metadata 'xep_0133', # Service Administration 'xep_0152', # Reachability Addresses 'xep_0153', # vCard-Based Avatars 'xep_0163', # Personal Eventing Protocol 'xep_0172', # User Nickname 'xep_0184', # Message Receipts 'xep_0186', # Invisible Command 'xep_0191', # Blocking Command 'xep_0196', # User Gaming 'xep_0198', # Stream Management 'xep_0199', # Ping 'xep_0202', # Entity Time 'xep_0203', # Delayed Delivery 'xep_0221', # Data Forms Media Element 'xep_0222', # Persistent Storage of Public Data via Pubsub 'xep_0223', # Persistent Storage of Private Data via Pubsub 'xep_0224', # Attention 'xep_0231', # Bits of Binary 'xep_0235', # OAuth Over XMPP 'xep_0242', # XMPP Client Compliance 2009 'xep_0249', # Direct MUC Invitations 'xep_0256', # Last Activity in Presence 'xep_0257', # Client Certificate Management for SASL EXTERNAL 'xep_0258', # Security Labels in XMPP 'xep_0270', # XMPP Compliance Suites 2010 'xep_0279', # Server IP Check 'xep_0280', # Message Carbons 'xep_0297', # Stanza Forwarding 'xep_0302', # XMPP Compliance Suites 2012 'xep_0308', # Last Message Correction 'xep_0313', # Message Archive Management 'xep_0319', # Last User Interaction in Presence 'xep_0323', # IoT Systems Sensor Data 'xep_0325', # IoT Systems Control 'xep_0332', # HTTP Over XMPP Transport ] slixmpp-1.2.2/slixmpp/plugins/xep_0334/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0334/__init__.py0000644000175000001440000000062312724610325022732 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0334.stanza import Store, NoStore, NoPermanentStore, NoCopy from slixmpp.plugins.xep_0334.hints import XEP_0334 register_plugin(XEP_0334) slixmpp-1.2.2/slixmpp/plugins/xep_0334/hints.py0000644000175000001440000000144312724610241022316 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2016 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0334 import stanza, Store, NoStore, NoPermanentStore, NoCopy log = logging.getLogger(__name__) class XEP_0334(BasePlugin): name = 'xep_0334' description = 'XEP-0334: Message Processing Hints' stanza = stanza def plugin_init(self): register_stanza_plugin(Message, Store) register_stanza_plugin(Message, NoStore) register_stanza_plugin(Message, NoPermanentStore) register_stanza_plugin(Message, NoCopy) slixmpp-1.2.2/slixmpp/plugins/xep_0334/stanza.py0000644000175000001440000000134512724607750022505 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Implementation of Message Processing Hints http://xmpp.org/extensions/xep-0334.html This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Store(ElementBase): name = 'store' plugin_attrib = 'store' namespace = 'urn:xmpp:hints' class NoPermanentStore(ElementBase): name = 'no-permanent-store' plugin_attrib = 'no-permanent-store' namespace = 'urn:xmpp:hints' class NoStore(ElementBase): name = 'no-store' plugin_attrib = 'no-store' namespace = 'urn:xmpp:hints' class NoCopy(ElementBase): name = 'no-copy' plugin_attrib = 'no-copy' namespace = 'urn:xmpp:hints' slixmpp-1.2.2/slixmpp/plugins/xep_0033/0000755000175000001440000000000013014656513020616 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0033/__init__.py0000644000175000001440000000065612770302340022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0033 import stanza from slixmpp.plugins.xep_0033.stanza import Addresses, Address from slixmpp.plugins.xep_0033.addresses import XEP_0033 register_plugin(XEP_0033) slixmpp-1.2.2/slixmpp/plugins/xep_0033/addresses.py0000644000175000001440000000165113004224717023144 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0033 import stanza, Addresses class XEP_0033(BasePlugin): """ XEP-0033: Extended Stanza Addressing """ name = 'xep_0033' description = 'XEP-0033: Extended Stanza Addressing' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, Addresses) register_stanza_plugin(Presence, Addresses) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Addresses.namespace) slixmpp-1.2.2/slixmpp/plugins/xep_0033/stanza.py0000644000175000001440000000712513004224717022471 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import JID, ElementBase, ET, register_stanza_plugin class Addresses(ElementBase): name = 'addresses' namespace = 'http://jabber.org/protocol/address' plugin_attrib = 'addresses' interfaces = set() def add_address(self, atype='to', jid='', node='', uri='', desc='', delivered=False): addr = Address(parent=self) addr['type'] = atype addr['jid'] = jid addr['node'] = node addr['uri'] = uri addr['desc'] = desc addr['delivered'] = delivered return addr # Additional methods for manipulating sets of addresses # based on type are generated below. class Address(ElementBase): name = 'address' namespace = 'http://jabber.org/protocol/address' plugin_attrib = 'address' interfaces = {'type', 'jid', 'node', 'uri', 'desc', 'delivered'} address_types = {'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'} def get_jid(self): return JID(self._get_attr('jid')) def set_jid(self, value): self._set_attr('jid', str(value)) def get_delivered(self): value = self._get_attr('delivered', False) return value and value.lower() in ('true', '1') def set_delivered(self, delivered): if delivered: self._set_attr('delivered', 'true') else: del self['delivered'] def set_uri(self, uri): if uri: del self['jid'] del self['node'] self._set_attr('uri', uri) else: self._del_attr('uri') # ===================================================================== # Auto-generate address type filters for the Addresses class. def _addr_filter(atype): def _type_filter(addr): if isinstance(addr, Address): if atype == 'all' or addr['type'] == atype: return True return False return _type_filter def _build_methods(atype): def get_multi(self): return list(filter(_addr_filter(atype), self)) def set_multi(self, value): del self[atype] for addr in value: # Support assigning dictionary versions of addresses # instead of full Address objects. if not isinstance(addr, Address): if atype != 'all': addr['type'] = atype elif 'atype' in addr and 'type' not in addr: addr['type'] = addr['atype'] addrObj = Address() addrObj.values = addr addr = addrObj self.append(addr) def del_multi(self): res = list(filter(_addr_filter(atype), self)) for addr in res: self.iterables.remove(addr) self.xml.remove(addr.xml) return get_multi, set_multi, del_multi for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'): get_multi, set_multi, del_multi = _build_methods(atype) Addresses.interfaces.add(atype) setattr(Addresses, "get_%s" % atype, get_multi) setattr(Addresses, "set_%s" % atype, set_multi) setattr(Addresses, "del_%s" % atype, del_multi) if atype == 'all': Addresses.interfaces.add('addresses') setattr(Addresses, "get_addresses", get_multi) setattr(Addresses, "set_addresses", set_multi) setattr(Addresses, "del_addresses", del_multi) register_stanza_plugin(Addresses, Address, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0122/0000755000175000001440000000000013014656513020615 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0122/__init__.py0000644000175000001440000000030712770302340022720 0ustar mathieuiusers00000000000000 from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0122.stanza import FormValidation from slixmpp.plugins.xep_0122.data_validation import XEP_0122 register_plugin(XEP_0122) slixmpp-1.2.2/slixmpp/plugins/xep_0122/data_validation.py0000644000175000001440000000102513004224717024304 0ustar mathieuiusers00000000000000from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import stanza from slixmpp.plugins.xep_0004.stanza import FormField from slixmpp.plugins.xep_0122.stanza import FormValidation class XEP_0122(BasePlugin): """ XEP-0122: Data Forms """ name = 'xep_0122' description = 'XEP-0122: Data Forms Validation' dependencies = {'xep_0004'} stanza = stanza def plugin_init(self): register_stanza_plugin(FormField, FormValidation) slixmpp-1.2.2/slixmpp/plugins/xep_0122/stanza.py0000644000175000001440000000533112603534742022473 0ustar mathieuiusers00000000000000from slixmpp.xmlstream import ElementBase, ET class FormValidation(ElementBase): """ Validation values for form fields. Example: 2003-10-06T11:22:00-07:00 Questions: Should this look at the datatype value and convert the range values as appropriate? Should this stanza provide a pass/fail for a value from the field, or convert field value to datatype? """ namespace = 'http://jabber.org/protocol/xdata-validate' name = 'validate' plugin_attrib = 'validate' interfaces = {'datatype', 'basic', 'open', 'range', 'regex', } sub_interfaces = {'basic', 'open', 'range', 'regex', } plugin_attrib_map = {} plugin_tag_map = {} def _add_field(self, name): self.remove_all() item_xml = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(item_xml) return item_xml def set_basic(self, value): if value: self._add_field('basic') else: del self['basic'] def set_open(self, value): if value: self._add_field('open') else: del self['open'] def set_regex(self, regex): if regex: _regex = self._add_field('regex') _regex.text = regex else: del self['regex'] def set_range(self, value, minimum=None, maximum=None): if value: _range = self._add_field('range') _range.attrib['min'] = str(minimum) _range.attrib['max'] = str(maximum) else: del self['range'] def remove_all(self, except_tag=None): for a in self.sub_interfaces: if a != except_tag: del self[a] def get_basic(self): present = self.xml.find('{%s}basic' % self.namespace) return present is not None def get_open(self): present = self.xml.find('{%s}open' % self.namespace) return present is not None def get_regex(self): present = self.xml.find('{%s}regex' % self.namespace) if present is not None: return present.text return False def get_range(self): present = self.xml.find('{%s}range' % self.namespace) if present is not None: attributes = present.attrib return_value = dict() if 'min' in attributes: return_value['minimum'] = attributes['min'] if 'max' in attributes: return_value['maximum'] = attributes['max'] return return_value return False slixmpp-1.2.2/slixmpp/plugins/xep_0054/0000755000175000001440000000000013014656513020621 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0054/__init__.py0000644000175000001440000000057212424504520022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0054.stanza import VCardTemp from slixmpp.plugins.xep_0054.vcard_temp import XEP_0054 register_plugin(XEP_0054) slixmpp-1.2.2/slixmpp/plugins/xep_0054/vcard_temp.py0000644000175000001440000001073113004224717023315 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import JID, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0054 import VCardTemp, stanza from slixmpp import future_wrapper log = logging.getLogger(__name__) class XEP_0054(BasePlugin): """ XEP-0054: vcard-temp """ name = 'xep_0054' description = 'XEP-0054: vcard-temp' dependencies = {'xep_0030', 'xep_0082'} stanza = stanza def plugin_init(self): """ Start the XEP-0054 plugin. """ register_stanza_plugin(Iq, VCardTemp) self.api.register(self._set_vcard, 'set_vcard', default=True) self.api.register(self._get_vcard, 'get_vcard', default=True) self.api.register(self._del_vcard, 'del_vcard', default=True) self._vcard_cache = {} self.xmpp.register_handler( Callback('VCardTemp', StanzaPath('iq/vcard_temp'), self._handle_get_vcard)) def plugin_end(self): self.xmpp.remove_handler('VCardTemp') self.xmpp['xep_0030'].del_feature(feature='vcard-temp') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('vcard-temp') def make_vcard(self): return VCardTemp() @future_wrapper def get_vcard(self, jid=None, ifrom=None, local=None, cached=False, callback=None, timeout=None, timeout_callback=None): if local is None: if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full elif jid in (None, ''): local = True if local: vcard = self.api['get_vcard'](jid, None, ifrom) if not isinstance(vcard, Iq): iq = self.xmpp.Iq() if vcard is None: vcard = VCardTemp() iq.append(vcard) return iq return vcard if cached: vcard = self.api['get_vcard'](jid, None, ifrom) if vcard is not None: if not isinstance(vcard, Iq): iq = self.xmpp.Iq() iq.append(vcard) return iq return vcard iq = self.xmpp.Iq() iq['to'] = jid iq['from'] = ifrom iq['type'] = 'get' iq.enable('vcard_temp') return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) @future_wrapper def publish_vcard(self, vcard=None, jid=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): self.api['set_vcard'](jid, None, ifrom, vcard) if self.xmpp.is_component: return iq = self.xmpp.Iq() iq['to'] = jid iq['from'] = ifrom iq['type'] = 'set' iq.append(vcard) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def _handle_get_vcard(self, iq): if iq['type'] == 'result': self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp']) return elif iq['type'] == 'get': vcard = self.api['get_vcard'](iq['from'].bare) if isinstance(vcard, Iq): vcard.send() else: iq = iq.reply() iq.append(vcard) iq.send() elif iq['type'] == 'set': raise XMPPError('service-unavailable') # ================================================================= def _set_vcard(self, jid, node, ifrom, vcard): self._vcard_cache[jid.bare] = vcard def _get_vcard(self, jid, node, ifrom, vcard): return self._vcard_cache.get(jid.bare, None) def _del_vcard(self, jid, node, ifrom, vcard): if jid.bare in self._vcard_cache: del self._vcard_cache[jid.bare] slixmpp-1.2.2/slixmpp/plugins/xep_0054/stanza.py0000644000175000001440000003547213004224717022502 0ustar mathieuiusers00000000000000import base64 import datetime as dt from slixmpp.util import bytes from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID from slixmpp.plugins import xep_0082 class VCardTemp(ElementBase): name = 'vCard' namespace = 'vcard-temp' plugin_attrib = 'vcard_temp' interfaces = {'FN', 'VERSION'} sub_interfaces = {'FN', 'VERSION'} class Name(ElementBase): name = 'N' namespace = 'vcard-temp' plugin_attrib = name interfaces = {'FAMILY', 'GIVEN', 'MIDDLE', 'PREFIX', 'SUFFIX'} sub_interfaces = interfaces def _set_component(self, name, value): if isinstance(value, list): value = ','.join(value) if value is not None: self._set_sub_text(name, value, keep=True) else: self._del_sub(name) def _get_component(self, name): value = self._get_sub_text(name, '') if ',' in value: value = [v.strip() for v in value.split(',')] return value def set_family(self, value): self._set_component('FAMILY', value) def get_family(self): return self._get_component('FAMILY') def set_given(self, value): self._set_component('GIVEN', value) def get_given(self): return self._get_component('GIVEN') def set_middle(self, value): print(value) self._set_component('MIDDLE', value) def get_middle(self): return self._get_component('MIDDLE') def set_prefix(self, value): self._set_component('PREFIX', value) def get_prefix(self): return self._get_component('PREFIX') def set_suffix(self, value): self._set_component('SUFFIX', value) def get_suffix(self): return self._get_component('SUFFIX') class Nickname(ElementBase): name = 'NICKNAME' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'nicknames' interfaces = {name} is_extension = True def set_nickname(self, value): if not value: self.xml.text = '' return if not isinstance(value, list): value = [value] self.xml.text = ','.join(value) def get_nickname(self): if self.xml.text: return self.xml.text.split(',') class Email(ElementBase): name = 'EMAIL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'emails' interfaces = {'HOME', 'WORK', 'INTERNET', 'PREF', 'X400', 'USERID'} sub_interfaces = {'USERID'} bool_interfaces = {'HOME', 'WORK', 'INTERNET', 'PREF', 'X400'} class Address(ElementBase): name = 'ADR' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'addresses' interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL', 'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY'} sub_interfaces = {'POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY'} bool_interfaces = {'HOME', 'WORK', 'DOM', 'INTL', 'PREF'} class Telephone(ElementBase): name = 'TEL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'telephone_numbers' interfaces = {'HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF', 'NUMBER'} sub_interfaces = {'NUMBER'} bool_interfaces = {'HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF'} def setup(self, xml=None): super().setup(xml=xml) ## this blanks out numbers received from server ##self._set_sub_text('NUMBER', '', keep=True) def set_number(self, value): self._set_sub_text('NUMBER', value, keep=True) def del_number(self): self._set_sub_text('NUMBER', '', keep=True) class Label(ElementBase): name = 'LABEL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'labels' interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF', 'lines'} bool_interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF'} def add_line(self, value): line = ET.Element('{%s}LINE' % self.namespace) line.text = value self.xml.append(line) def get_lines(self): lines = self.xml.find('{%s}LINE' % self.namespace) if lines is None: return [] return [line.text for line in lines] def set_lines(self, values): self.del_lines() for line in values: self.add_line(line) def del_lines(self): lines = self.xml.find('{%s}LINE' % self.namespace) if lines is None: return for line in lines: self.xml.remove(line) class Geo(ElementBase): name = 'GEO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'geolocations' interfaces = {'LAT', 'LON'} sub_interfaces = interfaces class Org(ElementBase): name = 'ORG' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'organizations' interfaces = {'ORGNAME', 'ORGUNIT', 'orgunits'} sub_interfaces = {'ORGNAME', 'ORGUNIT'} def add_orgunit(self, value): orgunit = ET.Element('{%s}ORGUNIT' % self.namespace) orgunit.text = value self.xml.append(orgunit) def get_orgunits(self): orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace) if orgunits is None: return [] return [orgunit.text for orgunit in orgunits] def set_orgunits(self, values): self.del_orgunits() for orgunit in values: self.add_orgunit(orgunit) def del_orgunits(self): orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace) if orgunits is None: return for orgunit in orgunits: self.xml.remove(orgunit) class Photo(ElementBase): name = 'PHOTO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'photos' interfaces = {'TYPE', 'EXTVAL'} sub_interfaces = interfaces class Logo(ElementBase): name = 'LOGO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'logos' interfaces = {'TYPE', 'EXTVAL'} sub_interfaces = interfaces class Sound(ElementBase): name = 'SOUND' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'sounds' interfaces = {'PHONETC', 'EXTVAL'} sub_interfaces = interfaces class BinVal(ElementBase): name = 'BINVAL' namespace = 'vcard-temp' plugin_attrib = name interfaces = {'BINVAL'} is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_binval(self, value): self.del_binval() parent = self.parent() if value: xml = ET.Element('{%s}BINVAL' % self.namespace) xml.text = bytes(base64.b64encode(value)).decode('utf-8') parent.append(xml) def get_binval(self): parent = self.parent() xml = parent.find('{%s}BINVAL' % self.namespace) if xml is not None: return base64.b64decode(bytes(xml.text)) return b'' def del_binval(self): self.parent()._del_sub('{%s}BINVAL' % self.namespace) class Classification(ElementBase): name = 'CLASS' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'classifications' interfaces = {'PUBLIC', 'PRIVATE', 'CONFIDENTIAL'} bool_interfaces = interfaces class Categories(ElementBase): name = 'CATEGORIES' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'categories' interfaces = {name} is_extension = True def set_categories(self, values): self.del_categories() for keyword in values: item = ET.Element('{%s}KEYWORD' % self.namespace) item.text = keyword self.xml.append(item) def get_categories(self): items = self.xml.findall('{%s}KEYWORD' % self.namespace) if items is None: return [] keywords = [] for item in items: keywords.append(item.text) return keywords def del_categories(self): items = self.xml.findall('{%s}KEYWORD' % self.namespace) for item in items: self.xml.remove(item) class Birthday(ElementBase): name = 'BDAY' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'birthdays' interfaces = {name} is_extension = True def set_bday(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self.xml.text = value def get_bday(self): if not self.xml.text: return None try: return xep_0082.parse(self.xml.text) except ValueError: return self.xml.text class Rev(ElementBase): name = 'REV' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'revision_dates' interfaces = {name} is_extension = True def set_rev(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self.xml.text = value def get_rev(self): if not self.xml.text: return None try: return xep_0082.parse(self.xml.text) except ValueError: return self.xml.text class Title(ElementBase): name = 'TITLE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'titles' interfaces = {name} is_extension = True def set_title(self, value): self.xml.text = value def get_title(self): return self.xml.text class Role(ElementBase): name = 'ROLE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'roles' interfaces = {name} is_extension = True def set_role(self, value): self.xml.text = value def get_role(self): return self.xml.text class Note(ElementBase): name = 'NOTE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'notes' interfaces = {name} is_extension = True def set_note(self, value): self.xml.text = value def get_note(self): return self.xml.text class Desc(ElementBase): name = 'DESC' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'descriptions' interfaces = {name} is_extension = True def set_desc(self, value): self.xml.text = value def get_desc(self): return self.xml.text class URL(ElementBase): name = 'URL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'urls' interfaces = {name} is_extension = True def set_url(self, value): self.xml.text = value def get_url(self): return self.xml.text class UID(ElementBase): name = 'UID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'uids' interfaces = {name} is_extension = True def set_uid(self, value): self.xml.text = value def get_uid(self): return self.xml.text class ProdID(ElementBase): name = 'PRODID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'product_ids' interfaces = {name} is_extension = True def set_prodid(self, value): self.xml.text = value def get_prodid(self): return self.xml.text class Mailer(ElementBase): name = 'MAILER' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'mailers' interfaces = {name} is_extension = True def set_mailer(self, value): self.xml.text = value def get_mailer(self): return self.xml.text class SortString(ElementBase): name = 'SORT-STRING' namespace = 'vcard-temp' plugin_attrib = 'SORT_STRING' plugin_multi_attrib = 'sort_strings' interfaces = {name} is_extension = True def set_sort_string(self, value): self.xml.text = value def get_sort_string(self): return self.xml.text class Agent(ElementBase): name = 'AGENT' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'agents' interfaces = {'EXTVAL'} sub_interfaces = interfaces class JabberID(ElementBase): name = 'JABBERID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'jids' interfaces = {name} is_extension = True def set_jabberid(self, value): self.xml.text = JID(value).bare def get_jabberid(self): return JID(self.xml.text) class TimeZone(ElementBase): name = 'TZ' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'timezones' interfaces = {name} is_extension = True def set_tz(self, value): time = xep_0082.time(offset=value) if time[-1] == 'Z': self.xml.text = 'Z' else: self.xml.text = time[-6:] def get_tz(self): if not self.xml.text: return xep_0082.tzutc() try: time = xep_0082.parse('00:00:00%s' % self.xml.text) return time.tzinfo except ValueError: return self.xml.text register_stanza_plugin(VCardTemp, Name) register_stanza_plugin(VCardTemp, Address, iterable=True) register_stanza_plugin(VCardTemp, Agent, iterable=True) register_stanza_plugin(VCardTemp, Birthday, iterable=True) register_stanza_plugin(VCardTemp, Categories, iterable=True) register_stanza_plugin(VCardTemp, Desc, iterable=True) register_stanza_plugin(VCardTemp, Email, iterable=True) register_stanza_plugin(VCardTemp, Geo, iterable=True) register_stanza_plugin(VCardTemp, JabberID, iterable=True) register_stanza_plugin(VCardTemp, Label, iterable=True) register_stanza_plugin(VCardTemp, Logo, iterable=True) register_stanza_plugin(VCardTemp, Mailer, iterable=True) register_stanza_plugin(VCardTemp, Note, iterable=True) register_stanza_plugin(VCardTemp, Nickname, iterable=True) register_stanza_plugin(VCardTemp, Org, iterable=True) register_stanza_plugin(VCardTemp, Photo, iterable=True) register_stanza_plugin(VCardTemp, ProdID, iterable=True) register_stanza_plugin(VCardTemp, Rev, iterable=True) register_stanza_plugin(VCardTemp, Role, iterable=True) register_stanza_plugin(VCardTemp, SortString, iterable=True) register_stanza_plugin(VCardTemp, Sound, iterable=True) register_stanza_plugin(VCardTemp, Telephone, iterable=True) register_stanza_plugin(VCardTemp, Title, iterable=True) register_stanza_plugin(VCardTemp, TimeZone, iterable=True) register_stanza_plugin(VCardTemp, UID, iterable=True) register_stanza_plugin(VCardTemp, URL, iterable=True) register_stanza_plugin(Photo, BinVal) register_stanza_plugin(Logo, BinVal) register_stanza_plugin(Sound, BinVal) register_stanza_plugin(Agent, VCardTemp) slixmpp-1.2.2/slixmpp/plugins/xep_0059/0000755000175000001440000000000013014656513020626 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0059/__init__.py0000644000175000001440000000060412770302340022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0059.stanza import Set from slixmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059 register_plugin(XEP_0059) slixmpp-1.2.2/slixmpp/plugins/xep_0059/rsm.py0000644000175000001440000001112513004224717021775 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp import Iq from slixmpp.plugins import BasePlugin, register_plugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0059 import stanza, Set from slixmpp.exceptions import XMPPError log = logging.getLogger(__name__) class ResultIterator(): """ An iterator for Result Set Managment """ def __init__(self, query, interface, results='substanzas', amount=10, start=None, reverse=False): """ Arguments: query -- The template query interface -- The substanza of the query, for example disco_items results -- The query stanza's interface which provides a countable list of query results. amount -- The max amounts of items to request per iteration start -- From which item id to start reverse -- If True, page backwards through the results Example: q = Iq() q['to'] = 'pubsub.example.com' q['disco_items']['node'] = 'blog' for i in ResultIterator(q, 'disco_items', '10'): print i['disco_items']['items'] """ self.query = query self.amount = amount self.start = start self.interface = interface self.results = results self.reverse = reverse self._stop = False def __iter__(self): return self def __next__(self): return self.next() def next(self): """ Return the next page of results from a query. Note: If using backwards paging, then the next page of results will be the items before the current page of items. """ if self._stop: raise StopIteration self.query[self.interface]['rsm']['before'] = self.reverse self.query['id'] = self.query.stream.new_id() self.query[self.interface]['rsm']['max'] = str(self.amount) if self.start and self.reverse: self.query[self.interface]['rsm']['before'] = self.start elif self.start: self.query[self.interface]['rsm']['after'] = self.start try: r = self.query.send(block=True) if not r[self.interface]['rsm']['first'] and \ not r[self.interface]['rsm']['last']: raise StopIteration if r[self.interface]['rsm']['count'] and \ r[self.interface]['rsm']['first_index']: count = int(r[self.interface]['rsm']['count']) first = int(r[self.interface]['rsm']['first_index']) num_items = len(r[self.interface][self.results]) if first + num_items == count: self._stop = True if self.reverse: self.start = r[self.interface]['rsm']['first'] else: self.start = r[self.interface]['rsm']['last'] return r except XMPPError: raise StopIteration class XEP_0059(BasePlugin): """ XEP-0050: Result Set Management """ name = 'xep_0059' description = 'XEP-0059: Result Set Management' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): """ Start the XEP-0059 plugin. """ register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems, self.stanza.Set) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Set.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Set.namespace) def iterate(self, stanza, interface, results='substanzas'): """ Create a new result set iterator for a given stanza query. Arguments: stanza -- A stanza object to serve as a template for queries made each iteration. For example, a basic disco#items query. interface -- The name of the substanza to which the result set management stanza should be appended. For example, for disco#items queries the interface 'disco_items' should be used. results -- The name of the interface containing the query results (typically just 'substanzas'). """ return ResultIterator(stanza, interface, results) slixmpp-1.2.2/slixmpp/plugins/xep_0059/stanza.py0000644000175000001440000000740713004224717022504 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins.xep_0030.stanza.items import DiscoItems class Set(ElementBase): """ XEP-0059 (Result Set Managment) can be used to manage the results of queries. For example, limiting the number of items per response or starting at certain positions. Example set stanzas: 2 conference.example.com pubsub.example.com Stanza Interface: first_index -- The index attribute of after -- The id defining from which item to start before -- The id defining from which item to start when browsing backwards max -- Max amount per response first -- Id for the first item in the response last -- Id for the last item in the response index -- Used to set an index to start from count -- The number of remote items available Methods: set_first_index -- Sets the index attribute for and creates the element if it doesn't exist get_first_index -- Returns the value of the index attribute for del_first_index -- Removes the index attribute for but keeps the element set_before -- Sets the value of , if the value is True then the element will be created without a value get_before -- Returns the value of , if it is empty it will return True """ namespace = 'http://jabber.org/protocol/rsm' name = 'set' plugin_attrib = 'rsm' sub_interfaces = {'first', 'after', 'before', 'count', 'index', 'last', 'max'} interfaces = {'first_index', 'first', 'after', 'before', 'count', 'index', 'last', 'max'} def set_first_index(self, val): fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: if val: fi.attrib['index'] = val elif 'index' in fi.attrib: del fi.attrib['index'] elif val: fi = ET.Element("{%s}first" % (self.namespace)) fi.attrib['index'] = val self.xml.append(fi) def get_first_index(self): fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: return fi.attrib.get('index', '') def del_first_index(self): fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: del fi.attrib['index'] def set_before(self, val): b = self.xml.find("{%s}before" % (self.namespace)) if b is None and val is True: self._set_sub_text('{%s}before' % self.namespace, '', True) else: self._set_sub_text('{%s}before' % self.namespace, val) def get_before(self): b = self.xml.find("{%s}before" % (self.namespace)) if b is not None and not b.text: return True elif b is not None: return b.text else: return None slixmpp-1.2.2/slixmpp/plugins/xep_0080/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0080/__init__.py0000644000175000001440000000057212424504520022727 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0080.stanza import Geoloc from slixmpp.plugins.xep_0080.geoloc import XEP_0080 register_plugin(XEP_0080) slixmpp-1.2.2/slixmpp/plugins/xep_0080/geoloc.py0000644000175000001440000001172213004224717022441 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp.plugins.base import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0080 import stanza, Geoloc log = logging.getLogger(__name__) class XEP_0080(BasePlugin): """ XEP-0080: User Location """ name = 'xep_0080' description = 'XEP-0080: User Location' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0163'].remove_interest(Geoloc.namespace) self.xmpp['xep_0030'].del_feature(feature=Geoloc.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_location', Geoloc) def publish_location(self, **kwargs): """ Publish the user's current location. Arguments: accuracy -- Horizontal GPS error in meters. alt -- Altitude in meters above or below sea level. area -- A named area such as a campus or neighborhood. bearing -- GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north. building -- A specific building on a street or in an area. country -- The nation where the user is located. countrycode -- The ISO 3166 two-letter country code. datum -- GPS datum. description -- A natural-language name for or description of the location. error -- Horizontal GPS error in arc minutes. Obsoleted by the accuracy parameter. floor -- A particular floor in a building. lat -- Latitude in decimal degrees North. locality -- A locality within the administrative region, such as a town or city. lon -- Longitude in decimal degrees East. postalcode -- A code used for postal delivery. region -- An administrative region of the nation, such as a state or province. room -- A particular room in a building. speed -- The speed at which the entity is moving, in meters per second. street -- A thoroughfare within the locality, or a crossing of two thoroughfares. text -- A catch-all element that captures any other information about the location. timestamp -- UTC timestamp specifying the moment when the reading was taken. uri -- A URI or URL pointing to information about the location. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ options = kwargs.get('options', None) ifrom = kwargs.get('ifrom', None) callback = kwargs.get('callback', None) timeout = kwargs.get('timeout', None) timeout_callback = kwargs.get('timeout_callback', None) for param in ('ifrom', 'block', 'callback', 'timeout', 'options', 'timeout_callback'): if param in kwargs: del kwargs[param] geoloc = Geoloc() geoloc.values = kwargs return self.xmpp['xep_0163'].publish(geoloc, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing user location information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ geoloc = Geoloc() return self.xmpp['xep_0163'].publish(geoloc, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=None) slixmpp-1.2.2/slixmpp/plugins/xep_0080/stanza.py0000644000175000001440000001767313004224717022504 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class Geoloc(ElementBase): """ XMPP's stanza allows entities to know the current geographical or physical location of an entity. (XEP-0080: User Location) Example stanzas: 20 Italy 45.44 Venice 12.33 Stanza Interface: accuracy -- Horizontal GPS error in meters. alt -- Altitude in meters above or below sea level. area -- A named area such as a campus or neighborhood. bearing -- GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north. building -- A specific building on a street or in an area. country -- The nation where the user is located. countrycode -- The ISO 3166 two-letter country code. datum -- GPS datum. description -- A natural-language name for or description of the location. error -- Horizontal GPS error in arc minutes. Obsoleted by the accuracy parameter. floor -- A particular floor in a building. lat -- Latitude in decimal degrees North. locality -- A locality within the administrative region, such as a town or city. lon -- Longitude in decimal degrees East. postalcode -- A code used for postal delivery. region -- An administrative region of the nation, such as a state or province. room -- A particular room in a building. speed -- The speed at which the entity is moving, in meters per second. street -- A thoroughfare within the locality, or a crossing of two thoroughfares. text -- A catch-all element that captures any other information about the location. timestamp -- UTC timestamp specifying the moment when the reading was taken. uri -- A URI or URL pointing to information about the location. """ namespace = 'http://jabber.org/protocol/geoloc' name = 'geoloc' interfaces = {'accuracy', 'alt', 'area', 'bearing', 'building', 'country', 'countrycode', 'datum', 'dscription', 'error', 'floor', 'lat', 'locality', 'lon', 'postalcode', 'region', 'room', 'speed', 'street', 'text', 'timestamp', 'uri'} sub_interfaces = interfaces plugin_attrib = name def exception(self, e): """ Override exception passback for presence. """ pass def set_accuracy(self, accuracy): """ Set the value of the element. Arguments: accuracy -- Horizontal GPS error in meters """ self._set_sub_text('accuracy', text=str(accuracy)) return self def get_accuracy(self): """ Return the value of the element as an integer. """ p = self._get_sub_text('accuracy') if not p: return None else: try: return int(p) except ValueError: return None def set_alt(self, alt): """ Set the value of the element. Arguments: alt -- Altitude in meters above or below sea level """ self._set_sub_text('alt', text=str(alt)) return self def get_alt(self): """ Return the value of the element as an integer. """ p = self._get_sub_text('alt') if not p: return None else: try: return int(p) except ValueError: return None def set_bearing(self, bearing): """ Set the value of the element. Arguments: bearing -- GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north """ self._set_sub_text('bearing', text=str(bearing)) return self def get_bearing(self): """ Return the value of the element as a float. """ p = self._get_sub_text('bearing') if not p: return None else: try: return float(p) except ValueError: return None def set_error(self, error): """ Set the value of the element. Arguments: error -- Horizontal GPS error in arc minutes; this element is deprecated in favor of """ self._set_sub_text('error', text=str(error)) return self def get_error(self): """ Return the value of the element as a float. """ p = self._get_sub_text('error') if not p: return None else: try: return float(p) except ValueError: return None def set_lat(self, lat): """ Set the value of the element. Arguments: lat -- Latitude in decimal degrees North """ self._set_sub_text('lat', text=str(lat)) return self def get_lat(self): """ Return the value of the element as a float. """ p = self._get_sub_text('lat') if not p: return None else: try: return float(p) except ValueError: return None def set_lon(self, lon): """ Set the value of the element. Arguments: lon -- Longitude in decimal degrees East """ self._set_sub_text('lon', text=str(lon)) return self def get_lon(self): """ Return the value of the element as a float. """ p = self._get_sub_text('lon') if not p: return None else: try: return float(p) except ValueError: return None def set_speed(self, speed): """ Set the value of the element. Arguments: speed -- The speed at which the entity is moving, in meters per second """ self._set_sub_text('speed', text=str(speed)) return self def get_speed(self): """ Return the value of the element as a float. """ p = self._get_sub_text('speed') if not p: return None else: try: return float(p) except ValueError: return None def set_timestamp(self, timestamp): """ Set the value of the element. Arguments: timestamp -- UTC timestamp specifying the moment when the reading was taken """ self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp))) return self def get_timestamp(self): """ Return the value of the element as a DateTime. """ p = self._get_sub_text('timestamp') if not p: return None else: return xep_0082.datetime(p) slixmpp-1.2.2/slixmpp/plugins/xep_0163.py0000644000175000001440000001076513004224717021201 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import asyncio from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin, register_plugin log = logging.getLogger(__name__) class XEP_0163(BasePlugin): """ XEP-0163: Personal Eventing Protocol (PEP) """ name = 'xep_0163' description = 'XEP-0163: Personal Eventing Protocol (PEP)' dependencies = {'xep_0030', 'xep_0060', 'xep_0115'} def register_pep(self, name, stanza): """ Setup and configure events and stanza registration for the given PEP stanza: - Add disco feature for the PEP content. - Register disco interest in the PEP content. - Map events from the PEP content's namespace to the given name. :param str name: The event name prefix to use for PEP events. :param stanza: The stanza class for the PEP content. """ pubsub_stanza = self.xmpp['xep_0060'].stanza register_stanza_plugin(pubsub_stanza.EventItem, stanza) self.add_interest(stanza.namespace) self.xmpp['xep_0030'].add_feature(stanza.namespace) self.xmpp['xep_0060'].map_node_event(stanza.namespace, name) def add_interest(self, namespace, jid=None): """ Mark an interest in a PEP subscription by including a disco feature with the '+notify' extension. Arguments: namespace -- The base namespace to register as an interest, such as 'http://jabber.org/protocol/tune'. This may also be a list of such namespaces. jid -- Optionally specify the JID. """ if not isinstance(namespace, set) and not isinstance(namespace, list): namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].add_feature('%s+notify' % ns, jid=jid) asyncio.async(self.xmpp['xep_0115'].update_caps(jid)) def remove_interest(self, namespace, jid=None): """ Mark an interest in a PEP subscription by including a disco feature with the '+notify' extension. Arguments: namespace -- The base namespace to remove as an interest, such as 'http://jabber.org/protocol/tune'. This may also be a list of such namespaces. jid -- Optionally specify the JID. """ if not isinstance(namespace, (set, list)): namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].del_feature(jid=jid, feature='%s+notify' % namespace) asyncio.async(self.xmpp['xep_0115'].update_caps(jid)) def publish(self, stanza, node=None, id=None, options=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Publish a PEP update. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. Arguments: stanza -- The PEP update stanza to publish. node -- The node to publish the item to. If not specified, the stanza's namespace will be used. id -- Optionally specify the ID of the item. options -- A form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if node is None: node = stanza.namespace if id is None: id = 'current' return self.xmpp['xep_0060'].publish(ifrom, node, id=id, payload=stanza.xml, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) register_plugin(XEP_0163) slixmpp-1.2.2/slixmpp/plugins/xep_0096/0000755000175000001440000000000013014656513020627 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0096/file_transfer.py0000644000175000001440000000336513004224717024027 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq, Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0096 import stanza, File log = logging.getLogger(__name__) class XEP_0096(BasePlugin): name = 'xep_0096' description = 'XEP-0096: SI File Transfer' dependencies = {'xep_0095'} stanza = stanza def plugin_init(self): register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File) self.xmpp['xep_0095'].register_profile(File.namespace, self) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(File.namespace) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=File.namespace) self.xmpp['xep_0095'].unregister_profile(File.namespace, self) def request_file_transfer(self, jid, sid=None, name=None, size=None, desc=None, hash=None, date=None, allow_ranged=False, mime_type=None, **iqargs): data = File() data['name'] = name data['size'] = size data['date'] = date data['desc'] = desc data['hash'] = hash if allow_ranged: data.enable('range') return self.xmpp['xep_0095'].offer(jid, sid=sid, mime_type=mime_type, profile=File.namespace, payload=data, **iqargs) slixmpp-1.2.2/slixmpp/plugins/xep_0096/__init__.py0000644000175000001440000000064412424504520022736 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0096 import stanza from slixmpp.plugins.xep_0096.stanza import File from slixmpp.plugins.xep_0096.file_transfer import XEP_0096 register_plugin(XEP_0096) slixmpp-1.2.2/slixmpp/plugins/xep_0096/stanza.py0000644000175000001440000000237113004224717022500 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp.xmlstream import ElementBase, register_stanza_plugin from slixmpp.plugins import xep_0082 class File(ElementBase): name = 'file' namespace = 'http://jabber.org/protocol/si/profile/file-transfer' plugin_attrib = 'file' interfaces = {'name', 'size', 'date', 'hash', 'desc'} sub_interfaces = {'desc'} def set_size(self, value): self._set_attr('size', str(value)) def get_date(self): timestamp = self._get_attr('date') return xep_0082.parse(timestamp) def set_date(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('date', value) class Range(ElementBase): name = 'range' namespace = 'http://jabber.org/protocol/si/profile/file-transfer' plugin_attrib = 'range' interfaces = {'length', 'offset'} def set_length(self, value): self._set_attr('length', str(value)) def set_offset(self, value): self._set_attr('offset', str(value)) register_stanza_plugin(File, Range) slixmpp-1.2.2/slixmpp/plugins/xep_0302.py0000644000175000001440000000103413004224717021161 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins import BasePlugin, register_plugin class XEP_0302(BasePlugin): name = 'xep_0302' description = 'XEP-0302: XMPP Compliance Suites 2012' dependencies = {'xep_0030', 'xep_0115', 'xep_0054', 'xep_0163', 'xep_0045', 'xep_0085', 'xep_0184', 'xep_0198'} register_plugin(XEP_0302) slixmpp-1.2.2/slixmpp/plugins/xep_0012/0000755000175000001440000000000013014656513020613 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0012/__init__.py0000644000175000001440000000060012770302340022712 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0012.stanza import LastActivity from slixmpp.plugins.xep_0012.last_activity import XEP_0012 register_plugin(XEP_0012) slixmpp-1.2.2/slixmpp/plugins/xep_0012/last_activity.py0000644000175000001440000001172213004224717024043 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from datetime import datetime, timedelta from slixmpp.plugins import BasePlugin, register_plugin from slixmpp import future_wrapper, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import JID, register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0012 import stanza, LastActivity log = logging.getLogger(__name__) class XEP_0012(BasePlugin): """ XEP-0012 Last Activity """ name = 'xep_0012' description = 'XEP-0012: Last Activity' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, LastActivity) self._last_activities = {} self.xmpp.register_handler( Callback('Last Activity', StanzaPath('iq@type=get/last_activity'), self._handle_get_last_activity)) self.api.register(self._default_get_last_activity, 'get_last_activity', default=True) self.api.register(self._default_set_last_activity, 'set_last_activity', default=True) self.api.register(self._default_del_last_activity, 'del_last_activity', default=True) def plugin_end(self): self.xmpp.remove_handler('Last Activity') self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('jabber:iq:last') def begin_idle(self, jid=None, status=None): self.set_last_activity(jid, 0, status) def end_idle(self, jid=None): self.del_last_activity(jid) def start_uptime(self, status=None): self.set_last_activity(None, 0, status) def set_last_activity(self, jid=None, seconds=None, status=None): self.api['set_last_activity'](jid, args={ 'seconds': seconds, 'status': status}) def del_last_activity(self, jid): self.api['del_last_activity'](jid) @future_wrapper def get_last_activity(self, jid, local=False, ifrom=None, timeout=None, callback=None, timeout_callback=None): if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full if local or jid in (None, ''): log.debug("Looking up local last activity data for %s", jid) return self.api['get_last_activity'](jid, None, ifrom, None) iq = self.xmpp.Iq() iq['from'] = ifrom iq['to'] = jid iq['type'] = 'get' iq.enable('last_activity') return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def _handle_get_last_activity(self, iq): log.debug("Received last activity query from " + \ "<%s> to <%s>.", iq['from'], iq['to']) reply = self.api['get_last_activity'](iq['to'], None, iq['from'], iq) reply.send() # ================================================================= # Default in-memory implementations for storing last activity data. # ================================================================= def _default_set_last_activity(self, jid, node, ifrom, data): seconds = data.get('seconds', None) if seconds is None: seconds = 0 status = data.get('status', None) if status is None: status = '' self._last_activities[jid] = { 'seconds': datetime.now() - timedelta(seconds=seconds), 'status': status} def _default_del_last_activity(self, jid, node, ifrom, data): if jid in self._last_activities: del self._last_activities[jid] def _default_get_last_activity(self, jid, node, ifrom, iq): if not isinstance(iq, Iq): reply = self.xmpp.Iq() else: reply = iq.reply() if jid not in self._last_activities: raise XMPPError('service-unavailable') bare = JID(jid).bare if bare != self.xmpp.boundjid.bare: if bare in self.xmpp.roster[jid]: sub = self.xmpp.roster[jid][bare]['subscription'] if sub not in ('from', 'both'): raise XMPPError('forbidden') td = datetime.now() - self._last_activities[jid]['seconds'] seconds = td.seconds + td.days * 24 * 3600 status = self._last_activities[jid]['status'] reply['last_activity']['seconds'] = seconds reply['last_activity']['status'] = status return reply slixmpp-1.2.2/slixmpp/plugins/xep_0012/stanza.py0000644000175000001440000000133013004224717022456 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class LastActivity(ElementBase): name = 'query' namespace = 'jabber:iq:last' plugin_attrib = 'last_activity' interfaces = {'seconds', 'status'} def get_seconds(self): return int(self._get_attr('seconds')) def set_seconds(self, value): self._set_attr('seconds', str(value)) def get_status(self): return self.xml.text def set_status(self, value): self.xml.text = str(value) def del_status(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/plugins/xep_0013/0000755000175000001440000000000013014656513020614 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0013/__init__.py0000644000175000001440000000056312424504517022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0013.stanza import Offline from slixmpp.plugins.xep_0013.offline import XEP_0013 register_plugin(XEP_0013) slixmpp-1.2.2/slixmpp/plugins/xep_0013/offline.py0000644000175000001440000000734613004224717022616 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging import slixmpp from slixmpp.stanza import Message, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Collector from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0013 import stanza log = logging.getLogger(__name__) class XEP_0013(BasePlugin): """ XEP-0013 Flexible Offline Message Retrieval """ name = 'xep_0013' description = 'XEP-0013: Flexible Offline Message Retrieval' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, stanza.Offline) register_stanza_plugin(Message, stanza.Offline) def get_count(self, **kwargs): return self.xmpp['xep_0030'].get_info( node='http://jabber.org/protocol/offline', local=False, **kwargs) def get_headers(self, **kwargs): return self.xmpp['xep_0030'].get_items( node='http://jabber.org/protocol/offline', local=False, **kwargs) def view(self, nodes, ifrom=None, timeout=None, callback=None, timeout_callback=None): if not isinstance(nodes, (list, set)): nodes = [nodes] iq = self.xmpp.Iq() iq['type'] = 'get' iq['from'] = ifrom offline = iq['offline'] for node in nodes: item = stanza.Item() item['node'] = node item['action'] = 'view' offline.append(item) collector = Collector( 'Offline_Results_%s' % iq['id'], StanzaPath('message/offline')) self.xmpp.register_handler(collector) def wrapped_cb(iq): results = collector.stop() if iq['type'] == 'result': iq['offline']['results'] = results callback(iq) iq.send(timeout=timeout, callback=wrapped_cb, timeout_callback=timeout_callback) def remove(self, nodes, ifrom=None, timeout=None, callback=None, timeout_callback=None): if not isinstance(nodes, (list, set)): nodes = [nodes] iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom offline = iq['offline'] for node in nodes: item = stanza.Item() item['node'] = node item['action'] = 'remove' offline.append(item) iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def fetch(self, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['offline']['fetch'] = True collector = Collector( 'Offline_Results_%s' % iq['id'], StanzaPath('message/offline')) self.xmpp.register_handler(collector) def wrapped_cb(iq): results = collector.stop() if iq['type'] == 'result': iq['offline']['results'] = results callback(iq) iq.send(timeout=timeout, callback=wrapped_cb, timeout_callback=timeout_callback) def purge(self, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['offline']['purge'] = True iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0013/stanza.py0000644000175000001440000000242513004224717022465 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Offline(ElementBase): name = 'offline' namespace = 'http://jabber.org/protocol/offline' plugin_attrib = 'offline' interfaces = {'fetch', 'purge', 'results'} bool_interfaces = interfaces def setup(self, xml=None): ElementBase.setup(self, xml) self._results = [] # The results interface is meant only as an easy # way to access the set of collected message responses # from the query. def get_results(self): return self._results def set_results(self, values): self._results = values def del_results(self): self._results = [] class Item(ElementBase): name = 'item' namespace = 'http://jabber.org/protocol/offline' plugin_attrib = 'item' interfaces = {'action', 'node', 'jid'} actions = {'view', 'remove'} def get_jid(self): return JID(self._get_attr('jid')) def set_jid(self, value): self._set_attr('jid', str(value)) register_stanza_plugin(Offline, Item, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0235/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0235/oauth.py0000644000175000001440000000143413004224717022312 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0235 import stanza, OAuth class XEP_0235(BasePlugin): name = 'xep_0235' description = 'XEP-0235: OAuth Over XMPP' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, OAuth) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:oauth:0') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:oauth:0') slixmpp-1.2.2/slixmpp/plugins/xep_0235/__init__.py0000644000175000001440000000063512424504520022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0235 import stanza from slixmpp.plugins.xep_0235.stanza import OAuth from slixmpp.plugins.xep_0235.oauth import XEP_0235 register_plugin(XEP_0235) slixmpp-1.2.2/slixmpp/plugins/xep_0235/stanza.py0000644000175000001440000000545113004224717022475 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import hmac import hashlib import urllib import base64 from slixmpp.xmlstream import ET, ElementBase, JID class OAuth(ElementBase): name = 'oauth' namespace = 'urn:xmpp:oauth:0' plugin_attrib = 'oauth' interfaces = {'oauth_consumer_key', 'oauth_nonce', 'oauth_signature', 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'} sub_interfaces = interfaces def generate_signature(self, stanza, sfrom, sto, consumer_secret, token_secret, method='HMAC-SHA1'): self['oauth_signature_method'] = method request = urllib.quote('%s&%s' % (sfrom, sto), '') parameters = urllib.quote('&'.join([ 'oauth_consumer_key=%s' % self['oauth_consumer_key'], 'oauth_nonce=%s' % self['oauth_nonce'], 'oauth_signature_method=%s' % self['oauth_signature_method'], 'oauth_timestamp=%s' % self['oauth_timestamp'], 'oauth_token=%s' % self['oauth_token'], 'oauth_version=%s' % self['oauth_version'] ]), '') sigbase = '%s&%s&%s' % (stanza, request, parameters) consumer_secret = urllib.quote(consumer_secret, '') token_secret = urllib.quote(token_secret, '') key = '%s&%s' % (consumer_secret, token_secret) if method == 'HMAC-SHA1': sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest()) elif method == 'PLAINTEXT': sig = key self['oauth_signature'] = sig return sig def verify_signature(self, stanza, sfrom, sto, consumer_secret, token_secret): method = self['oauth_signature_method'] request = urllib.quote('%s&%s' % (sfrom, sto), '') parameters = urllib.quote('&'.join([ 'oauth_consumer_key=%s' % self['oauth_consumer_key'], 'oauth_nonce=%s' % self['oauth_nonce'], 'oauth_signature_method=%s' % self['oauth_signature_method'], 'oauth_timestamp=%s' % self['oauth_timestamp'], 'oauth_token=%s' % self['oauth_token'], 'oauth_version=%s' % self['oauth_version'] ]), '') sigbase = '%s&%s&%s' % (stanza, request, parameters) consumer_secret = urllib.quote(consumer_secret, '') token_secret = urllib.quote(token_secret, '') key = '%s&%s' % (consumer_secret, token_secret) if method == 'HMAC-SHA1': sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest()) elif method == 'PLAINTEXT': sig = key return self['oauth_signature'] == sig slixmpp-1.2.2/slixmpp/plugins/xep_0085/0000755000175000001440000000000013014656513020625 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0085/__init__.py0000644000175000001440000000057112770302340022733 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0085.stanza import ChatState from slixmpp.plugins.xep_0085.chat_states import XEP_0085 register_plugin(XEP_0085) slixmpp-1.2.2/slixmpp/plugins/xep_0085/chat_states.py0000644000175000001440000000316713004224717023504 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging import slixmpp from slixmpp.stanza import Message from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0085 import stanza, ChatState log = logging.getLogger(__name__) class XEP_0085(BasePlugin): """ XEP-0085 Chat State Notifications """ name = 'xep_0085' description = 'XEP-0085: Chat State Notifications' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Chat State', StanzaPath('message/chat_state'), self._handle_chat_state)) register_stanza_plugin(Message, stanza.Active) register_stanza_plugin(Message, stanza.Composing) register_stanza_plugin(Message, stanza.Gone) register_stanza_plugin(Message, stanza.Inactive) register_stanza_plugin(Message, stanza.Paused) def plugin_end(self): self.xmpp.remove_handler('Chat State') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace) def _handle_chat_state(self, msg): state = msg['chat_state'] log.debug("Chat State: %s, %s", state, msg['from'].jid) self.xmpp.event('chatstate', msg) self.xmpp.event('chatstate_%s' % state, msg) slixmpp-1.2.2/slixmpp/plugins/xep_0085/stanza.py0000644000175000001440000000423113004224717022473 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import slixmpp from slixmpp.xmlstream import ElementBase, ET class ChatState(ElementBase): """ Example chat state stanzas: Stanza Interfaces: chat_state Attributes: states Methods: get_chat_state set_chat_state del_chat_state """ name = '' namespace = 'http://jabber.org/protocol/chatstates' plugin_attrib = 'chat_state' interfaces = {'chat_state'} sub_interfaces = interfaces is_extension = True states = {'active', 'composing', 'gone', 'inactive', 'paused'} def setup(self, xml=None): self.xml = ET.Element('') return True def get_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.xml.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = state_xml return state return '' def set_chat_state(self, state): self.del_chat_state() parent = self.parent() if state in self.states: self.xml = ET.Element('{%s}%s' % (self.namespace, state)) parent.append(self.xml) elif state not in [None, '']: raise ValueError('Invalid chat state') def del_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.xml.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = ET.Element('') parent.xml.remove(state_xml) class Active(ChatState): name = 'active' class Composing(ChatState): name = 'composing' class Gone(ChatState): name = 'gone' class Inactive(ChatState): name = 'inactive' class Paused(ChatState): name = 'paused' slixmpp-1.2.2/slixmpp/plugins/xep_0107/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0107/__init__.py0000644000175000001440000000064412424504520022727 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0107 import stanza from slixmpp.plugins.xep_0107.stanza import UserMood from slixmpp.plugins.xep_0107.user_mood import XEP_0107 register_plugin(XEP_0107) slixmpp-1.2.2/slixmpp/plugins/xep_0107/user_mood.py0000644000175000001440000000622113004224717023163 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0107 import stanza, UserMood log = logging.getLogger(__name__) class XEP_0107(BasePlugin): """ XEP-0107: User Mood """ name = 'xep_0107' description = 'XEP-0107: User Mood' dependencies = {'xep_0163'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, UserMood) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserMood.namespace) self.xmpp['xep_0163'].remove_interest(UserMood.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_mood', UserMood) def publish_mood(self, value=None, text=None, options=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Publish the user's current mood. Arguments: value -- The name of the mood to publish. text -- Optional natural-language description or reason for the mood. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ mood = UserMood() mood['value'] = value mood['text'] = text self.xmpp['xep_0163'].publish(mood, node=UserMood.namespace, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing user mood information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ mood = UserMood() self.xmpp['xep_0163'].publish(mood, node=UserMood.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0107/stanza.py0000644000175000001440000000424613004224717022474 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class UserMood(ElementBase): name = 'mood' namespace = 'http://jabber.org/protocol/mood' plugin_attrib = 'mood' interfaces = {'value', 'text'} sub_interfaces = {'text'} moods = {'afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious', 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious', 'cold', 'confident', 'confused', 'contemplative', 'contented', 'cranky', 'crazy', 'creative', 'curious', 'dejected', 'depressed', 'disappointed', 'disgusted', 'dismayed', 'distracted', 'embarrassed', 'envious', 'excited', 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy', 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated', 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love', 'indignant', 'interested', 'intoxicated', 'invincible', 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody', 'nervous', 'neutral', 'offended', 'outraged', 'playful', 'proud', 'relaxed', 'relieved', 'remorseful', 'restless', 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked', 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong', 'surprised', 'thankful', 'thirsty', 'tired', 'undefined', 'weak', 'worried'} def set_value(self, value): self.del_value() if value in self.moods: self._set_sub_text(value, '', keep=True) else: raise ValueError('Unknown mood value') def get_value(self): for child in self.xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.moods: return elem_name return '' def del_value(self): curr_value = self.get_value() if curr_value: self._set_sub_text(curr_value, '', keep=False) slixmpp-1.2.2/slixmpp/plugins/xep_0084/0000755000175000001440000000000013014656513020624 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0084/__init__.py0000644000175000001440000000064712424504520022736 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0084 import stanza from slixmpp.plugins.xep_0084.stanza import Data, MetaData from slixmpp.plugins.xep_0084.avatar import XEP_0084 register_plugin(XEP_0084) slixmpp-1.2.2/slixmpp/plugins/xep_0084/avatar.py0000644000175000001440000000764513004224717022464 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import hashlib import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0084 import stanza, Data, MetaData log = logging.getLogger(__name__) class XEP_0084(BasePlugin): name = 'xep_0084' description = 'XEP-0084: User Avatar' dependencies = {'xep_0163', 'xep_0060'} stanza = stanza def plugin_init(self): pubsub_stanza = self.xmpp['xep_0060'].stanza register_stanza_plugin(pubsub_stanza.Item, Data) register_stanza_plugin(pubsub_stanza.EventItem, Data) self.xmpp['xep_0060'].map_node_event(Data.namespace, 'avatar_data') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=MetaData.namespace) self.xmpp['xep_0163'].remove_interest(MetaData.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData) def generate_id(self, data): return hashlib.sha1(data).hexdigest() def retrieve_avatar(self, jid, id, url=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def publish_avatar(self, data, ifrom=None, callback=None, timeout=None, timeout_callback=None): payload = Data() payload['value'] = data return self.xmpp['xep_0163'].publish(payload, id=self.generate_id(data), ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def publish_avatar_metadata(self, items=None, pointers=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): metadata = MetaData() if items is None: items = [] if not isinstance(items, (list, set)): items = [items] for info in items: metadata.add_info(info['id'], info['type'], info['bytes'], height=info.get('height', ''), width=info.get('width', ''), url=info.get('url', '')) if pointers is not None: for pointer in pointers: metadata.add_pointer(pointer) return self.xmpp['xep_0163'].publish(metadata, id=info['id'], ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing avatar metadata information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ metadata = MetaData() return self.xmpp['xep_0163'].publish(metadata, node=MetaData.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0084/stanza.py0000644000175000001440000000400413004224717022470 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from base64 import b64encode, b64decode from slixmpp.util import bytes from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Data(ElementBase): name = 'data' namespace = 'urn:xmpp:avatar:data' plugin_attrib = 'avatar_data' interfaces = {'value'} def get_value(self): if self.xml.text: return b64decode(bytes(self.xml.text)) return b'' def set_value(self, value): if value: self.xml.text = b64encode(bytes(value)).decode() else: self.xml.text = '' def del_value(self): self.xml.text = '' class MetaData(ElementBase): name = 'metadata' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'avatar_metadata' interfaces = set() def add_info(self, id, itype, ibytes, height=None, width=None, url=None): info = Info() info.values = {'id': id, 'type': itype, 'bytes': '%s' % ibytes, 'height': height, 'width': width, 'url': url} self.append(info) def add_pointer(self, xml): if not isinstance(xml, Pointer): pointer = Pointer() pointer.append(xml) self.append(pointer) else: self.append(xml) class Info(ElementBase): name = 'info' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'info' plugin_multi_attrib = 'items' interfaces = {'bytes', 'height', 'id', 'type', 'url', 'width'} class Pointer(ElementBase): name = 'pointer' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'pointer' plugin_multi_attrib = 'pointers' interfaces = set() register_stanza_plugin(MetaData, Info, iterable=True) register_stanza_plugin(MetaData, Pointer, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0045.py0000644000175000001440000003666413004224717021206 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import with_statement import logging from slixmpp import Presence from slixmpp.plugins import BasePlugin, register_plugin from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.xpath import MatchXPath from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask from slixmpp.exceptions import IqError, IqTimeout log = logging.getLogger(__name__) class MUCPresence(ElementBase): name = 'x' namespace = 'http://jabber.org/protocol/muc#user' plugin_attrib = 'muc' interfaces = {'affiliation', 'role', 'jid', 'nick', 'room'} affiliations = {'', } roles = {'', } def get_xml_item(self): item = self.xml.find('{http://jabber.org/protocol/muc#user}item') if item is None: item = ET.Element('{http://jabber.org/protocol/muc#user}item') self.xml.append(item) return item def get_affiliation(self): #TODO if no affilation, set it to the default and return default item = self.get_xml_item() return item.get('affiliation', '') def set_affiliation(self, value): item = self.get_xml_item() #TODO check for valid affiliation item.attrib['affiliation'] = value return self def del_affiliation(self): item = self.get_xml_item() #TODO set default affiliation if 'affiliation' in item.attrib: del item.attrib['affiliation'] return self def get_jid(self): item = self.get_xml_item() return JID(item.get('jid', '')) def set_jid(self, value): item = self.get_xml_item() if not isinstance(value, str): value = str(value) item.attrib['jid'] = value return self def del_jid(self): item = self.get_xml_item() if 'jid' in item.attrib: del item.attrib['jid'] return self def get_role(self): item = self.get_xml_item() #TODO get default role, set default role if none return item.get('role', '') def set_role(self, value): item = self.get_xml_item() #TODO check for valid role item.attrib['role'] = value return self def del_role(self): item = self.get_xml_item() #TODO set default role if 'role' in item.attrib: del item.attrib['role'] return self def get_nick(self): return self.parent()['from'].resource def get_room(self): return self.parent()['from'].bare def set_nick(self, value): log.warning("Cannot set nick through mucpresence plugin.") return self def set_room(self, value): log.warning("Cannot set room through mucpresence plugin.") return self def del_nick(self): log.warning("Cannot delete nick through mucpresence plugin.") return self def del_room(self): log.warning("Cannot delete room through mucpresence plugin.") return self class XEP_0045(BasePlugin): """ Implements XEP-0045 Multi-User Chat """ name = 'xep_0045' description = 'XEP-0045: Multi-User Chat' dependencies = {'xep_0030', 'xep_0004'} def plugin_init(self): self.rooms = {} self.our_nicks = {} self.xep = '0045' # load MUC support in presence stanzas register_stanza_plugin(Presence, MUCPresence) self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_error_message)) self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_message)) self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_subject)) self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("" % self.xmpp.default_ns), self.handle_config_change)) self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( self.xmpp.default_ns, 'http://jabber.org/protocol/muc#user', 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite)) def plugin_end(self): self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc') def handle_groupchat_invite(self, inv): """ Handle an invite into a muc. """ logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv) if inv['from'] not in self.rooms.keys(): self.xmpp.event("groupchat_invite", inv) def handle_config_change(self, msg): """Handle a MUC configuration change (with status code).""" self.xmpp.event('groupchat_config_status', msg) self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg) def handle_groupchat_presence(self, pr): """ Handle a presence in a muc. """ got_offline = False got_online = False if pr['muc']['room'] not in self.rooms.keys(): return self.xmpp.roster[pr['from']].ignore_updates = True entry = pr['muc'].get_stanza_values() entry['show'] = pr['show'] entry['status'] = pr['status'] entry['alt_nick'] = pr['nick'] if pr['type'] == 'unavailable': if entry['nick'] in self.rooms[entry['room']]: del self.rooms[entry['room']][entry['nick']] got_offline = True else: if entry['nick'] not in self.rooms[entry['room']]: got_online = True self.rooms[entry['room']][entry['nick']] = entry log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry) self.xmpp.event("groupchat_presence", pr) self.xmpp.event("muc::%s::presence" % entry['room'], pr) if got_offline: self.xmpp.event("muc::%s::got_offline" % entry['room'], pr) if got_online: self.xmpp.event("muc::%s::got_online" % entry['room'], pr) def handle_groupchat_message(self, msg): """ Handle a message event in a muc. """ self.xmpp.event('groupchat_message', msg) self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) def handle_groupchat_error_message(self, msg): """ Handle a message error event in a muc. """ self.xmpp.event('groupchat_message_error', msg) self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg) def handle_groupchat_subject(self, msg): """ Handle a message coming from a muc indicating a change of subject (or announcing it when joining the room) """ self.xmpp.event('groupchat_subject', msg) def jid_in_room(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return True return False def get_nick(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return nick def configure_room(self, room, form=None, ifrom=None): if form is None: form = self.get_room_config(room, ifrom=ifrom) iq = self.xmpp.make_iq_set() iq['to'] = room if ifrom is not None: iq['from'] = ifrom query = ET.Element('{http://jabber.org/protocol/muc#owner}query') form['type'] = 'submit' query.append(form) iq.append(query) # For now, swallow errors to preserve existing API try: result = iq.send() except IqError: return False except IqTimeout: return False return True def join_muc(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.make_presence(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('{http://jabber.org/protocol/muc}password') passelement.text = password x.append(passelement) if maxhistory: history = ET.Element('{http://jabber.org/protocol/muc}history') if maxhistory == "0": history.attrib['maxchars'] = maxhistory else: history.attrib['maxstanzas'] = maxhistory x.append(history) stanza.append(x) if not wait: self.xmpp.send(stanza) else: #wait for our own room presence back expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)}) self.xmpp.send(stanza, expect) self.rooms[room] = {} self.our_nicks[room] = nick def destroy(self, room, reason='', altroom = '', ifrom=None): iq = self.xmpp.make_iq_set() if ifrom is not None: iq['from'] = ifrom iq['to'] = room query = ET.Element('{http://jabber.org/protocol/muc#owner}query') destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy') if altroom: destroy.attrib['jid'] = altroom xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason') xreason.text = reason destroy.append(xreason) query.append(destroy) iq.append(query) # For now, swallow errors to preserve existing API try: r = iq.send() except IqError: return False except IqTimeout: return False return True def set_affiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None): """ Change room affiliation.""" if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if nick is not None: item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) else: item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid}) query.append(item) iq = self.xmpp.make_iq_set(query) iq['to'] = room iq['from'] = ifrom # For now, swallow errors to preserve existing API try: result = iq.send() except IqError: return False except IqTimeout: return False return True def set_role(self, room, nick, role): """ Change role property of a nick in a room. Typically, roles are temporary (they last only as long as you are in the room), whereas affiliations are permanent (they last across groupchat sessions). """ if role not in ('moderator', 'participant', 'visitor', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') item = ET.Element('item', {'role':role, 'nick':nick}) query.append(item) iq = self.xmpp.make_iq_set(query) iq['to'] = room result = iq.send() if result is False or result['type'] != 'result': raise ValueError return True def invite(self, room, jid, reason='', mfrom=''): """ Invite a jid to a room.""" msg = self.xmpp.make_message(room) msg['from'] = mfrom x = ET.Element('{http://jabber.org/protocol/muc#user}x') invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) if reason: rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason') rxml.text = reason invite.append(rxml) x.append(invite) msg.append(x) self.xmpp.send(msg) def leave_muc(self, room, nick, msg='', pfrom=None): """ Leave the specified room. """ if msg: self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom) else: self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom) del self.rooms[room] def get_room_config(self, room, ifrom=''): iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner') iq['to'] = room iq['from'] = ifrom # For now, swallow errors to preserve existing API try: result = iq.send() 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: raise ValueError return self.xmpp.plugin['xep_0004'].build_form(form) def cancel_config(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.make_iq_set(query) iq['to'] = room iq['from'] = ifrom iq.send() def set_room_config(self, room, config, ifrom=''): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') config['type'] = 'submit' query.append(config) iq = self.xmpp.make_iq_set(query) iq['to'] = room iq['from'] = ifrom iq.send() def get_joined_rooms(self): return self.rooms.keys() def get_our_jid_in_room(self, room_jid): """ Return the jid we're using in a room. """ return "%s/%s" % (room_jid, self.our_nicks[room_jid]) def get_jid_property(self, room, nick, jid_property): """ Get the property of a nick in a room, such as its 'jid' or 'affiliation' If not found, return None. """ if room in self.rooms and nick in self.rooms[room] and jid_property in self.rooms[room][nick]: return self.rooms[room][nick][jid_property] else: return None def get_roster(self, room): """ Get the list of nicks in a room. """ if room not in self.rooms.keys(): return None return self.rooms[room].keys() def get_users_by_affiliation(cls, room, affiliation='member', ifrom=None): if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation}) query.append(item) iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get') iq.append(query) return iq.send() register_plugin(XEP_0045) slixmpp-1.2.2/slixmpp/plugins/xep_0256.py0000644000175000001440000000432513004224717021177 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Presence from slixmpp.exceptions import XMPPError from slixmpp.plugins import BasePlugin, register_plugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0012 import stanza, LastActivity log = logging.getLogger(__name__) class XEP_0256(BasePlugin): name = 'xep_0256' description = 'XEP-0256: Last Activity in Presence' dependencies = {'xep_0012'} stanza = stanza default_config = { 'auto_last_activity': False } def plugin_init(self): register_stanza_plugin(Presence, LastActivity) self.xmpp.add_filter('out', self._initial_presence_activity) self.xmpp.add_event_handler('connected', self._reset_presence_activity) self._initial_presence = set() def plugin_end(self): self.xmpp.del_filter('out', self._initial_presence_activity) self.xmpp.del_event_handler('connected', self._reset_presence_activity) def _reset_presence_activity(self, e): self._initial_presence = set() def _initial_presence_activity(self, stanza): if isinstance(stanza, Presence): use_last_activity = False if self.auto_last_activity and stanza['show'] in ('xa', 'away'): use_last_activity = True if stanza['from'] not in self._initial_presence: self._initial_presence.add(stanza['from']) use_last_activity = True if use_last_activity: plugin = self.xmpp['xep_0012'] try: result = plugin.api['get_last_activity'](stanza['from'], None, stanza['to']) seconds = result['last_activity']['seconds'] except XMPPError: seconds = None if seconds is not None: stanza['last_activity']['seconds'] = seconds return stanza register_plugin(XEP_0256) slixmpp-1.2.2/slixmpp/plugins/xep_0027/0000755000175000001440000000000013014656513020621 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0027/__init__.py0000644000175000001440000000057312424504517022737 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0027.stanza import Signed, Encrypted from slixmpp.plugins.xep_0027.gpg import XEP_0027 register_plugin(XEP_0027) slixmpp-1.2.2/slixmpp/plugins/xep_0027/gpg.py0000644000175000001440000001316312774761314021764 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.thirdparty import GPG from slixmpp.stanza import Presence, Message from slixmpp.plugins.base import BasePlugin, register_plugin from slixmpp.xmlstream import ElementBase, register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0027 import stanza, Signed, Encrypted def _extract_data(data, kind): stripped = [] begin_headers = False begin_data = False for line in data.split('\n'): if not begin_headers and 'BEGIN PGP %s' % kind in line: begin_headers = True continue if begin_headers and line.strip() == '': begin_data = True continue if 'END PGP %s' % kind in line: return '\n'.join(stripped) if begin_data: stripped.append(line) return '' class XEP_0027(BasePlugin): name = 'xep_0027' description = 'XEP-0027: Current Jabber OpenPGP Usage' dependencies = set() stanza = stanza default_config = { 'gpg_binary': 'gpg', 'gpg_home': '', 'use_agent': True, 'keyring': None, 'key_server': 'pgp.mit.edu' } def plugin_init(self): self.gpg = GPG(gnupghome=self.gpg_home, gpgbinary=self.gpg_binary, use_agent=self.use_agent, keyring=self.keyring) self.xmpp.add_filter('out', self._sign_presence) self._keyids = {} self.api.register(self._set_keyid, 'set_keyid', default=True) self.api.register(self._get_keyid, 'get_keyid', default=True) self.api.register(self._del_keyid, 'del_keyid', default=True) self.api.register(self._get_keyids, 'get_keyids', default=True) register_stanza_plugin(Presence, Signed) register_stanza_plugin(Message, Encrypted) self.xmpp.add_event_handler('unverified_signed_presence', self._handle_unverified_signed_presence) self.xmpp.register_handler( Callback('Signed Presence', StanzaPath('presence/signed'), self._handle_signed_presence)) self.xmpp.register_handler( Callback('Encrypted Message', StanzaPath('message/encrypted'), self._handle_encrypted_message)) def plugin_end(self): self.xmpp.remove_handler('Encrypted Message') self.xmpp.remove_handler('Signed Presence') self.xmpp.del_filter('out', self._sign_presence) self.xmpp.del_event_handler('unverified_signed_presence', self._handle_unverified_signed_presence) def _sign_presence(self, stanza): if isinstance(stanza, Presence): if stanza['type'] == 'available' or \ stanza['type'] in Presence.showtypes: stanza['signed'] = stanza['status'] return stanza def sign(self, data, jid=None): keyid = self.get_keyid(jid) if keyid: signed = self.gpg.sign(data, keyid=keyid) return _extract_data(signed.data, 'SIGNATURE') def encrypt(self, data, jid=None): keyid = self.get_keyid(jid) if keyid: enc = self.gpg.encrypt(data, keyid) return _extract_data(enc.data, 'MESSAGE') def decrypt(self, data, jid=None): template = '-----BEGIN PGP MESSAGE-----\n' + \ '\n' + \ '%s\n' + \ '-----END PGP MESSAGE-----\n' dec = self.gpg.decrypt(template % data) return dec.data def verify(self, data, sig, jid=None): template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \ 'Hash: SHA1\n' + \ '\n' + \ '%s\n' + \ '-----BEGIN PGP SIGNATURE-----\n' + \ '\n' + \ '%s\n' + \ '-----END PGP SIGNATURE-----\n' v = self.gpg.verify(template % (data, sig)) return v def set_keyid(self, jid=None, keyid=None): self.api['set_keyid'](jid, args=keyid) def get_keyid(self, jid=None): return self.api['get_keyid'](jid) def del_keyid(self, jid=None): self.api['del_keyid'](jid) def get_keyids(self): return self.api['get_keyids']() def _handle_signed_presence(self, pres): self.xmpp.event('unverified_signed_presence', pres) def _handle_unverified_signed_presence(self, pres): verified = self.verify(pres['status'], pres['signed']) if verified.key_id: if not self.get_keyid(pres['from']): known_keyids = [e['keyid'] for e in self.gpg.list_keys()] if verified.key_id not in known_keyids: self.gpg.recv_keys(self.key_server, verified.key_id) self.set_keyid(jid=pres['from'], keyid=verified.key_id) self.xmpp.event('signed_presence', pres) def _handle_encrypted_message(self, msg): self.xmpp.event('encrypted_message', msg) # ================================================================= def _set_keyid(self, jid, node, ifrom, keyid): self._keyids[jid] = keyid def _get_keyid(self, jid, node, ifrom, keyid): return self._keyids.get(jid, None) def _del_keyid(self, jid, node, ifrom, keyid): if jid in self._keyids: del self._keyids[jid] def _get_keyids(self, jid, node, ifrom, data): return self._keyids slixmpp-1.2.2/slixmpp/plugins/xep_0027/stanza.py0000644000175000001440000000246713004224717022500 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Signed(ElementBase): name = 'x' namespace = 'jabber:x:signed' plugin_attrib = 'signed' interfaces = {'signed'} is_extension = True def set_signed(self, value): parent = self.parent() xmpp = parent.stream data = xmpp['xep_0027'].sign(value, parent['from']) if data: self.xml.text = data else: del parent['signed'] def get_signed(self): return self.xml.text class Encrypted(ElementBase): name = 'x' namespace = 'jabber:x:encrypted' plugin_attrib = 'encrypted' interfaces = {'encrypted'} is_extension = True def set_encrypted(self, value): parent = self.parent() xmpp = parent.stream data = xmpp['xep_0027'].encrypt(value, parent['to']) if data: self.xml.text = data else: del parent['encrypted'] def get_encrypted(self): parent = self.parent() xmpp = parent.stream if self.xml.text: return xmpp['xep_0027'].decrypt(self.xml.text, parent['to']) return None slixmpp-1.2.2/slixmpp/plugins/xep_0172/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0172/__init__.py0000644000175000001440000000064412424504520022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0172 import stanza from slixmpp.plugins.xep_0172.stanza import UserNick from slixmpp.plugins.xep_0172.user_nick import XEP_0172 register_plugin(XEP_0172) slixmpp-1.2.2/slixmpp/plugins/xep_0172/user_nick.py0000644000175000001440000000621013004224717023151 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza.message import Message from slixmpp.stanza.presence import Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0172 import stanza, UserNick log = logging.getLogger(__name__) class XEP_0172(BasePlugin): """ XEP-0172: User Nickname """ name = 'xep_0172' description = 'XEP-0172: User Nickname' dependencies = {'xep_0163'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, UserNick) register_stanza_plugin(Presence, UserNick) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserNick.namespace) self.xmpp['xep_0163'].remove_interest(UserNick.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_nick', UserNick) def publish_nick(self, nick=None, options=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Publish the user's current nick. Arguments: nick -- The user nickname to publish. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ nickname = UserNick() nickname['nick'] = nick self.xmpp['xep_0163'].publish(nickname, node=UserNick.namespace, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Clear existing user nick information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ nick = UserNick() return self.xmpp['xep_0163'].publish(nick, node=UserNick.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0172/stanza.py0000644000175000001440000000401013004224717022463 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class UserNick(ElementBase): """ XEP-0172: User Nickname allows the addition of a element in several stanza types, including and stanzas. The nickname contained in a should be the global, friendly or informal name chosen by the owner of a bare JID. The element may be included when establishing communications with new entities, such as normal XMPP users or MUC services. The nickname contained in a element will not necessarily be the same as the nickname used in a MUC. Example stanzas: The User ... The User Stanza Interface: nick -- A global, friendly or informal name chosen by a user. Methods: setup -- Overrides ElementBase.setup. get_nick -- Return the nickname in the element. set_nick -- Add a element with the given nickname. del_nick -- Remove the element. """ namespace = 'http://jabber.org/protocol/nick' name = 'nick' plugin_attrib = name interfaces = {'nick'} def set_nick(self, nick): """ Add a element with the given nickname. Arguments: nick -- A human readable, informal name. """ self.xml.text = nick def get_nick(self): """Return the nickname in the element.""" return self.xml.text def del_nick(self): """Remove the element.""" if self.parent is not None: self.parent().xml.remove(self.xml) slixmpp-1.2.2/slixmpp/plugins/xep_0133.py0000644000175000001440000000355213004224717021172 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins import BasePlugin, register_plugin class XEP_0133(BasePlugin): name = 'xep_0133' description = 'XEP-0133: Service Administration' dependencies = {'xep_0030', 'xep_0004', 'xep_0050'} commands = {'add-user', 'delete-user', 'disable-user', 'reenable-user', 'end-user-session', 'get-user-password', 'change-user-password', 'get-user-roster', 'get-user-lastlogin', 'user-stats', 'edit-blacklist', 'edit-whitelist', 'get-registered-users-num', 'get-disabled-users-num', 'get-online-users-num', 'get-active-users-num', 'get-idle-users-num', 'get-registered-users-list', 'get-disabled-users-list', 'get-online-users-list', 'get-online-users', 'get-active-users', 'get-idle-userslist', 'announce', 'set-motd', 'edit-motd', 'delete-motd', 'set-welcome', 'delete-welcome', 'edit-admin', 'restart', 'shutdown'} def get_commands(self, jid=None, **kwargs): if jid is None: jid = self.xmpp.boundjid.server return self.xmpp['xep_0050'].get_commands(jid, **kwargs) def create_command(name): def admin_command(self, jid=None, session=None, ifrom=None): if jid is None: jid = self.xmpp.boundjid.server self.xmpp['xep_0050'].start_command( jid=jid, node='http://jabber.org/protocol/admin#%s' % name, session=session, ifrom=ifrom) return admin_command for cmd in XEP_0133.commands: setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd)) register_plugin(XEP_0133) slixmpp-1.2.2/slixmpp/plugins/xep_0065/0000755000175000001440000000000013014656513020623 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0065/proxy.py0000644000175000001440000002447013004224717022361 0ustar mathieuiusers00000000000000import asyncio import logging import socket from hashlib import sha1 from uuid import uuid4 from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0065 import stanza, Socks5, Socks5Protocol log = logging.getLogger(__name__) class XEP_0065(BasePlugin): name = 'xep_0065' description = "XEP-0065: SOCKS5 Bytestreams" dependencies = {'xep_0030'} default_config = { 'auto_accept': False } def plugin_init(self): register_stanza_plugin(Iq, Socks5) self._proxies = {} self._sessions = {} self._preauthed_sids = {} self.xmpp.register_handler( Callback('Socks5 Bytestreams', StanzaPath('iq@type=set/socks/streamhost'), self._handle_streamhost)) self.api.register(self._authorized, 'authorized', default=True) self.api.register(self._authorized_sid, 'authorized_sid', default=True) self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Socks5.namespace) def plugin_end(self): self.xmpp.remove_handler('Socks5 Bytestreams') self.xmpp.remove_handler('Socks5 Streamhost Used') self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace) def get_socket(self, sid): """Returns the socket associated to the SID.""" return self._sessions.get(sid, None) def handshake(self, to, ifrom=None, sid=None, timeout=None): """ Starts the handshake to establish the socks5 bytestreams connection. """ if not self._proxies: self._proxies = yield from self.discover_proxies() if sid is None: sid = uuid4().hex used = yield from self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) proxy = used['socks']['streamhost_used']['jid'] if proxy not in self._proxies: log.warning('Received unknown SOCKS5 proxy: %s', proxy) return try: self._sessions[sid] = (yield from self._connect_proxy( self._get_dest_sha1(sid, self.xmpp.boundjid, to), self._proxies[proxy][0], self._proxies[proxy][1]))[1] except socket.error: return None addr, port = yield from self._sessions[sid].connected # Request that the proxy activate the session with the target. yield from self.activate(proxy, sid, to, timeout=timeout) sock = self.get_socket(sid) self.xmpp.event('stream:%s:%s' % (sid, to), sock) return sock def request_stream(self, to, sid=None, ifrom=None, timeout=None, callback=None): if sid is None: sid = uuid4().hex # Requester initiates S5B negotiation with Target by sending # IQ-set that includes the JabberID and network address of # StreamHost as well as the StreamID (SID) of the proposed # bytestream. iq = self.xmpp.Iq() iq['to'] = to iq['from'] = ifrom iq['type'] = 'set' iq['socks']['sid'] = sid for proxy, (host, port) in self._proxies.items(): iq['socks'].add_streamhost(proxy, host, port) return iq.send(timeout=timeout, callback=callback) def discover_proxies(self, jid=None, ifrom=None, timeout=None): """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" if jid is None: if self.xmpp.is_component: jid = self.xmpp.server else: jid = self.xmpp.boundjid.server discovered = set() disco_items = yield from self.xmpp['xep_0030'].get_items(jid, timeout=timeout) disco_items = {item[0] for item in disco_items['disco_items']['items']} disco_info_futures = {} for item in disco_items: disco_info_futures[item] = self.xmpp['xep_0030'].get_info(item, timeout=timeout) for item in disco_items: try: disco_info = yield from disco_info_futures[item] except XMPPError: continue else: # Verify that the identity is a bytestream proxy. identities = disco_info['disco_info']['identities'] for identity in identities: if identity[0] == 'proxy' and identity[1] == 'bytestreams': discovered.add(disco_info['from']) for jid in discovered: try: addr = yield from self.get_network_address(jid, ifrom=ifrom, timeout=timeout) self._proxies[jid] = (addr['socks']['streamhost']['host'], addr['socks']['streamhost']['port']) except XMPPError: continue return self._proxies def get_network_address(self, proxy, ifrom=None, timeout=None, callback=None): """Get the network information of a proxy.""" iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom) iq.enable('socks') return iq.send(timeout=timeout, callback=callback) def _get_dest_sha1(self, sid, requester, target): # The hostname MUST be SHA1(SID + Requester JID + Target JID) # where the output is hexadecimal-encoded (not binary). digest = sha1() digest.update(sid.encode('utf8')) digest.update(str(requester).encode('utf8')) digest.update(str(target).encode('utf8')) return digest.hexdigest() def _handle_streamhost(self, iq): """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] if not sid: raise XMPPError(etype='modify', condition='bad-request') if not self._accept_stream(iq): raise XMPPError(etype='modify', condition='not-acceptable') streamhosts = iq['socks']['streamhosts'] requester = iq['from'] target = iq['to'] dest = self._get_dest_sha1(sid, requester, target) proxy_futures = [] for streamhost in streamhosts: proxy_futures.append(self._connect_proxy( dest, streamhost['host'], streamhost['port'])) @asyncio.coroutine def gather(futures, iq, streamhosts): proxies = yield from asyncio.gather(*futures, return_exceptions=True) for streamhost, proxy in zip(streamhosts, proxies): if isinstance(proxy, ValueError): continue elif isinstance(proxy, socket.error): log.error('Socket error while connecting to the proxy.') continue proxy = proxy[1] # TODO: what if the future never happens? try: addr, port = yield from proxy.connected except socket.error: log.exception('Socket error while connecting to the proxy.') continue # TODO: make a better choice than just the first working one. used_streamhost = streamhost['jid'] conn = proxy break else: raise XMPPError(etype='cancel', condition='item-not-found') # TODO: close properly the connection to the other proxies. iq = iq.reply() self._sessions[sid] = conn iq['socks']['sid'] = sid iq['socks']['streamhost_used']['jid'] = used_streamhost iq.send() self.xmpp.event('socks5_stream', conn) self.xmpp.event('stream:%s:%s' % (sid, requester), conn) asyncio.async(gather(proxy_futures, iq, streamhosts)) def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None): """Activate the socks5 session that has been negotiated.""" iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom) iq['socks']['sid'] = sid iq['socks']['activate'] = target return iq.send(timeout=timeout, callback=callback) def deactivate(self, sid): """Closes the proxy socket associated with this SID.""" sock = self._sessions.get(sid) if sock: try: # sock.close() will also delete sid from self._sessions (see _connect_proxy) sock.close() except socket.error: pass # Though this should not be neccessary remove the closed session anyway if sid in self._sessions: log.warn(('SOCKS5 session with sid = "%s" was not ' + 'removed from _sessions by sock.close()') % sid) del self._sessions[sid] def close(self): """Closes all proxy sockets.""" for sid, sock in self._sessions.items(): sock.close() self._sessions = {} def _connect_proxy(self, dest, proxy, proxy_port): """ Returns a future to a connection between the client and the server-side Socks5 proxy. dest : The SHA-1 of (SID + Requester JID + Target JID), in hex. host : The hostname or the IP of the proxy. port : The port of the proxy. or """ factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event) return self.xmpp.loop.create_connection(factory, proxy, proxy_port) def _accept_stream(self, iq): receiver = iq['to'] sender = iq['from'] sid = iq['socks']['sid'] if self.api['authorized_sid'](receiver, sid, sender, iq): return True return self.api['authorized'](receiver, sid, sender, iq) def _authorized(self, jid, sid, ifrom, iq): return self.auto_accept def _authorized_sid(self, jid, sid, ifrom, iq): log.debug('>>> authed sids: %s', self._preauthed_sids) log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) if (jid, sid, ifrom) in self._preauthed_sids: del self._preauthed_sids[(jid, sid, ifrom)] return True return False def _preauthorize_sid(self, jid, sid, ifrom, data): log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) self._preauthed_sids[(jid, sid, ifrom)] = True slixmpp-1.2.2/slixmpp/plugins/xep_0065/__init__.py0000644000175000001440000000036012572154127022735 0ustar mathieuiusers00000000000000from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0065.socks5 import Socks5Protocol from slixmpp.plugins.xep_0065.stanza import Socks5 from slixmpp.plugins.xep_0065.proxy import XEP_0065 register_plugin(XEP_0065) slixmpp-1.2.2/slixmpp/plugins/xep_0065/socks5.py0000644000175000001440000001676512572154127022425 0ustar mathieuiusers00000000000000'''Pure asyncio implementation of RFC 1928 - SOCKS Protocol Version 5.''' import asyncio import enum import logging import socket import struct from slixmpp.stringprep import punycode, StringprepError log = logging.getLogger(__name__) class ProtocolMismatch(Exception): '''We only implement SOCKS5, no other version or protocol.''' class ProtocolError(Exception): '''Some protocol error.''' class MethodMismatch(Exception): '''The server answered with a method we didn’t ask for.''' class MethodUnacceptable(Exception): '''None of our methods is supported by the server.''' class AddressTypeUnacceptable(Exception): '''The address type (ATYP) field isn’t one of IPv4, IPv6 or domain name.''' class ReplyError(Exception): '''The server answered with an error.''' possible_values = ( "succeeded", "general SOCKS server failure", "connection not allowed by ruleset", "Network unreachable", "Host unreachable", "Connection refused", "TTL expired", "Command not supported", "Address type not supported", "Unknown error") def __init__(self, result): if result < 9: Exception.__init__(self, self.possible_values[result]) else: Exception.__init__(self, self.possible_values[9]) class Method(enum.IntEnum): '''Known methods for a SOCKS5 session.''' none = 0 gssapi = 1 password = 2 # Methods 3 to 127 are reserved by IANA. # Methods 128 to 254 are reserved for private use. unacceptable = 255 not_yet_selected = -1 class Command(enum.IntEnum): '''Existing commands for requests.''' connect = 1 bind = 2 udp_associate = 3 class AddressType(enum.IntEnum): '''Existing address types.''' ipv4 = 1 domain = 3 ipv6 = 4 class Socks5Protocol(asyncio.Protocol): '''This implements SOCKS5 as an asyncio protocol.''' def __init__(self, dest_addr, dest_port, event): self.methods = {Method.none} self.selected_method = Method.not_yet_selected self.transport = None self.dest = (dest_addr, dest_port) self.connected = asyncio.Future() self.event = event self.paused = asyncio.Future() self.paused.set_result(None) def register_method(self, method): '''Register a SOCKS5 method.''' self.methods.add(method) def unregister_method(self, method): '''Unregister a SOCKS5 method.''' self.methods.remove(method) def connection_made(self, transport): '''Called when the connection to the SOCKS5 server is established.''' log.debug('SOCKS5 connection established.') self.transport = transport self._send_methods() def data_received(self, data): '''Called when we received some data from the SOCKS5 server.''' log.debug('SOCKS5 message received.') # If we are already connected, this is a data packet. if self.connected.done(): return self.event('socks5_data', data) # Every SOCKS5 message starts with the protocol version. if data[0] != 5: raise ProtocolMismatch() # Then select the correct handler for the data we just received. if self.selected_method == Method.not_yet_selected: self._handle_method(data) else: self._handle_connect(data) def connection_lost(self, exc): log.debug('SOCKS5 connection closed.') self.event('socks5_closed', exc) def pause_writing(self): self.paused = asyncio.Future() def resume_writing(self): self.paused.set_result(None) def write(self, data): yield from self.paused self.transport.write(data) def _send_methods(self): '''Send the methods request, first thing a client should do.''' # Create the buffer for our request. request = bytearray(len(self.methods) + 2) # Protocol version. request[0] = 5 # Number of methods to send. request[1] = len(self.methods) # List every method we support. for i, method in enumerate(self.methods): request[i + 2] = method # Send the request. self.transport.write(request) def _send_request(self, command): '''Send a request, should be done after having negociated a method.''' # Encode the destination address to embed it in our request. # We need to do that first because its length is variable. address, port = self.dest addr = self._encode_addr(address) # Create the buffer for our request. request = bytearray(5 + len(addr)) # Protocol version. request[0] = 5 # Specify the command we want to use. request[1] = command # request[2] is reserved, keeping it at 0. # Add our destination address and port. request[3:3+len(addr)] = addr request[-2:] = struct.pack('>H', port) # Send the request. log.debug('SOCKS5 message sent.') self.transport.write(request) def _handle_method(self, data): '''Handle a method reply from the server.''' if len(data) != 2: raise ProtocolError() selected_method = data[1] if selected_method not in self.methods: raise MethodMismatch() if selected_method == Method.unacceptable: raise MethodUnacceptable() self.selected_method = selected_method self._send_request(Command.connect) def _handle_connect(self, data): '''Handle a connect reply from the server.''' try: addr, port = self._parse_result(data) except ReplyError as exception: self.connected.set_exception(exception) self.connected.set_result((addr, port)) self.event('socks5_connected', (addr, port)) def _parse_result(self, data): '''Parse a reply from the server.''' result = data[1] if result != 0: raise ReplyError(result) addr = self._parse_addr(data[3:-2]) port = struct.unpack('>H', data[-2:])[0] return (addr, port) @staticmethod def _parse_addr(addr): '''Parse an address (IP or domain) from a bytestream.''' addr_type = addr[0] if addr_type == AddressType.ipv6: try: return socket.inet_ntop(socket.AF_INET6, addr[1:]) except ValueError as e: raise AddressTypeUnacceptable(e) if addr_type == AddressType.ipv4: try: return socket.inet_ntop(socket.AF_INET, addr[1:]) except ValueError as e: raise AddressTypeUnacceptable(e) if addr_type == AddressType.domain: length = addr[1] address = addr[2:] if length != len(address): raise Exception('Size mismatch') return address.decode() raise AddressTypeUnacceptable(addr_type) @staticmethod def _encode_addr(addr): '''Encode an address (IP or domain) into a bytestream.''' try: ipv6 = socket.inet_pton(socket.AF_INET6, addr) return b'\x04' + ipv6 except OSError: pass try: ipv4 = socket.inet_aton(addr) return b'\x01' + ipv4 except OSError: pass try: domain = punycode(addr) return b'\x03' + bytes([len(domain)]) + domain except StringprepError: pass raise Exception('Err…') slixmpp-1.2.2/slixmpp/plugins/xep_0065/stanza.py0000644000175000001440000000237013004224717022473 0ustar mathieuiusers00000000000000from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Socks5(ElementBase): name = 'query' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'socks' interfaces = {'sid', 'activate'} sub_interfaces = {'activate'} def add_streamhost(self, jid, host, port): sh = StreamHost(parent=self) sh['jid'] = jid sh['host'] = host sh['port'] = port class StreamHost(ElementBase): name = 'streamhost' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost' plugin_multi_attrib = 'streamhosts' interfaces = {'host', 'jid', 'port'} def set_jid(self, value): return self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class StreamHostUsed(ElementBase): name = 'streamhost-used' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost_used' interfaces = {'jid'} def set_jid(self, value): return self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Socks5, StreamHost, iterable=True) register_stanza_plugin(Socks5, StreamHostUsed) slixmpp-1.2.2/slixmpp/plugins/xep_0020/0000755000175000001440000000000013014656513020612 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0020/__init__.py0000644000175000001440000000067012424504517022726 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0020 import stanza from slixmpp.plugins.xep_0020.stanza import FeatureNegotiation from slixmpp.plugins.xep_0020.feature_negotiation import XEP_0020 register_plugin(XEP_0020) slixmpp-1.2.2/slixmpp/plugins/xep_0020/feature_negotiation.py0000644000175000001440000000176513004224717025224 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq, Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0020 import stanza, FeatureNegotiation from slixmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) class XEP_0020(BasePlugin): name = 'xep_0020' description = 'XEP-0020: Feature Negotiation' dependencies = {'xep_0004', 'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace) register_stanza_plugin(FeatureNegotiation, Form) register_stanza_plugin(Iq, FeatureNegotiation) register_stanza_plugin(Message, FeatureNegotiation) slixmpp-1.2.2/slixmpp/plugins/xep_0020/stanza.py0000644000175000001440000000063012424504517022463 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class FeatureNegotiation(ElementBase): name = 'feature' namespace = 'http://jabber.org/protocol/feature-neg' plugin_attrib = 'feature_neg' interfaces = set() slixmpp-1.2.2/slixmpp/plugins/xep_0047/0000755000175000001440000000000013014656513020623 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0047/__init__.py0000644000175000001440000000074012770302340022727 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0047 import stanza from slixmpp.plugins.xep_0047.stanza import Open, Close, Data from slixmpp.plugins.xep_0047.stream import IBBytestream from slixmpp.plugins.xep_0047.ibb import XEP_0047 register_plugin(XEP_0047) slixmpp-1.2.2/slixmpp/plugins/xep_0047/ibb.py0000644000175000001440000001454013004224717021731 0ustar mathieuiusers00000000000000import asyncio import uuid import logging from slixmpp import Message, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream log = logging.getLogger(__name__) class XEP_0047(BasePlugin): name = 'xep_0047' description = 'XEP-0047: In-band Bytestreams' dependencies = {'xep_0030'} stanza = stanza default_config = { 'block_size': 4096, 'max_block_size': 8192, 'auto_accept': False, } def plugin_init(self): self._streams = {} self._preauthed_sids = {} register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Data) register_stanza_plugin(Message, Data) self.xmpp.register_handler(Callback( 'IBB Open', StanzaPath('iq@type=set/ibb_open'), self._handle_open_request)) self.xmpp.register_handler(Callback( 'IBB Close', StanzaPath('iq@type=set/ibb_close'), self._handle_close)) self.xmpp.register_handler(Callback( 'IBB Data', StanzaPath('iq@type=set/ibb_data'), self._handle_data)) self.xmpp.register_handler(Callback( 'IBB Message Data', StanzaPath('message/ibb_data'), self._handle_data)) self.api.register(self._authorized, 'authorized', default=True) self.api.register(self._authorized_sid, 'authorized_sid', default=True) self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) self.api.register(self._get_stream, 'get_stream', default=True) self.api.register(self._set_stream, 'set_stream', default=True) self.api.register(self._del_stream, 'del_stream', default=True) def plugin_end(self): self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Close') self.xmpp.remove_handler('IBB Data') self.xmpp.remove_handler('IBB Message Data') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') def _get_stream(self, jid, sid, peer_jid, data): return self._streams.get((jid, sid, peer_jid), None) def _set_stream(self, jid, sid, peer_jid, stream): self._streams[(jid, sid, peer_jid)] = stream def _del_stream(self, jid, sid, peer_jid, data): if (jid, sid, peer_jid) in self._streams: del self._streams[(jid, sid, peer_jid)] def _accept_stream(self, iq): receiver = iq['to'] sender = iq['from'] sid = iq['ibb_open']['sid'] if self.api['authorized_sid'](receiver, sid, sender, iq): return True return self.api['authorized'](receiver, sid, sender, iq) def _authorized(self, jid, sid, ifrom, iq): if self.auto_accept: return True return False def _authorized_sid(self, jid, sid, ifrom, iq): if (jid, sid, ifrom) in self._preauthed_sids: del self._preauthed_sids[(jid, sid, ifrom)] return True return False def _preauthorize_sid(self, jid, sid, ifrom, data): self._preauthed_sids[(jid, sid, ifrom)] = True def open_stream(self, jid, block_size=None, sid=None, use_messages=False, ifrom=None, timeout=None, callback=None): if sid is None: sid = str(uuid.uuid4()) if block_size is None: block_size = self.block_size iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom iq['ibb_open']['block_size'] = block_size iq['ibb_open']['sid'] = sid iq['ibb_open']['stanza'] = 'message' if use_messages else 'iq' stream = IBBytestream(self.xmpp, sid, block_size, iq['from'], iq['to'], use_messages) stream_future = asyncio.Future() def _handle_opened_stream(iq): log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from']) stream.self_jid = iq['to'] stream.peer_jid = iq['from'] stream.stream_started = True self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) stream_future.set_result(stream) if callback is not None: callback(stream) self.xmpp.event('ibb_stream_start', stream) self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream) iq.send(timeout=timeout, callback=_handle_opened_stream) return stream_future def _handle_open_request(self, iq): sid = iq['ibb_open']['sid'] size = iq['ibb_open']['block_size'] or self.block_size log.debug('Received IBB stream request from %s', iq['from']) if not sid: raise XMPPError(etype='modify', condition='bad-request') if not self._accept_stream(iq): raise XMPPError(etype='cancel', condition='not-acceptable') if size > self.max_block_size: raise XMPPError('resource-constraint') stream = IBBytestream(self.xmpp, sid, size, iq['to'], iq['from']) stream.stream_started = True self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) iq.reply().send() self.xmpp.event('ibb_stream_start', stream) self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream) def _handle_data(self, stanza): sid = stanza['ibb_data']['sid'] stream = self.api['get_stream'](stanza['to'], sid, stanza['from']) if stream is not None and stanza['from'] == stream.peer_jid: stream._recv_data(stanza) else: raise XMPPError('item-not-found') def _handle_close(self, iq): sid = iq['ibb_close']['sid'] stream = self.api['get_stream'](iq['to'], sid, iq['from']) if stream is not None and iq['from'] == stream.peer_jid: stream._closed(iq) self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid) else: raise XMPPError('item-not-found') slixmpp-1.2.2/slixmpp/plugins/xep_0047/stream.py0000644000175000001440000000725612572154127022504 0ustar mathieuiusers00000000000000import asyncio import socket import logging from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError log = logging.getLogger(__name__) class IBBytestream(object): def __init__(self, xmpp, sid, block_size, jid, peer, use_messages=False): self.xmpp = xmpp self.sid = sid self.block_size = block_size self.use_messages = use_messages if jid is None: jid = xmpp.boundjid self.self_jid = jid self.peer_jid = peer self.send_seq = -1 self.recv_seq = -1 self.stream_started = False self.stream_in_closed = False self.stream_out_closed = False self.recv_queue = asyncio.Queue() @asyncio.coroutine def send(self, data, timeout=None): if not self.stream_started or self.stream_out_closed: raise socket.error if len(data) > self.block_size: data = data[:self.block_size] self.send_seq = (self.send_seq + 1) % 65535 seq = self.send_seq if self.use_messages: msg = self.xmpp.Message() msg['to'] = self.peer_jid msg['from'] = self.self_jid msg['id'] = self.xmpp.new_id() msg['ibb_data']['sid'] = self.sid msg['ibb_data']['seq'] = seq msg['ibb_data']['data'] = data msg.send() else: iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = self.peer_jid iq['from'] = self.self_jid iq['ibb_data']['sid'] = self.sid iq['ibb_data']['seq'] = seq iq['ibb_data']['data'] = data yield from iq.send(timeout=timeout) return len(data) @asyncio.coroutine def sendall(self, data, timeout=None): sent_len = 0 while sent_len < len(data): sent_len += yield from self.send(data[sent_len:self.block_size], timeout=timeout) @asyncio.coroutine def sendfile(self, file, timeout=None): while True: data = file.read(self.block_size) if not data: break yield from self.send(data, timeout=timeout) def _recv_data(self, stanza): new_seq = stanza['ibb_data']['seq'] if new_seq != (self.recv_seq + 1) % 65535: self.close() raise XMPPError('unexpected-request') self.recv_seq = new_seq data = stanza['ibb_data']['data'] if len(data) > self.block_size: self.close() raise XMPPError('not-acceptable') self.recv_queue.put_nowait(data) self.xmpp.event('ibb_stream_data', self) if isinstance(stanza, Iq): stanza.reply().send() def recv(self, *args, **kwargs): return self.read() def read(self): if not self.stream_started or self.stream_in_closed: raise socket.error return self.recv_queue.get_nowait() def close(self, timeout=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = self.peer_jid iq['from'] = self.self_jid iq['ibb_close']['sid'] = self.sid self.stream_out_closed = True def _close_stream(_): self.stream_in_closed = True future = iq.send(timeout=timeout, callback=_close_stream) self.xmpp.event('ibb_stream_end', self) return future def _closed(self, iq): self.stream_in_closed = True self.stream_out_closed = True iq.reply().send() self.xmpp.event('ibb_stream_end', self) def makefile(self, *args, **kwargs): return self def connect(*args, **kwargs): return None def shutdown(self, *args, **kwargs): return None slixmpp-1.2.2/slixmpp/plugins/xep_0047/stanza.py0000644000175000001440000000325413004224717022475 0ustar mathieuiusers00000000000000import re import base64 from slixmpp.util import bytes from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import ElementBase VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*') def to_b64(data): return bytes(base64.b64encode(bytes(data))).decode('utf-8') def from_b64(data): return bytes(base64.b64decode(bytes(data))) class Open(ElementBase): name = 'open' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_open' interfaces = {'block_size', 'sid', 'stanza'} def get_block_size(self): return int(self._get_attr('block-size', '0')) def set_block_size(self, value): self._set_attr('block-size', str(value)) def del_block_size(self): self._del_attr('block-size') class Data(ElementBase): name = 'data' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_data' interfaces = {'seq', 'sid', 'data'} sub_interfaces = {'data'} def get_seq(self): return int(self._get_attr('seq', '0')) def set_seq(self, value): self._set_attr('seq', str(value)) def get_data(self): text = self.xml.text if not text: raise XMPPError('not-acceptable', 'IBB data element is empty.') b64_data = text.strip() if VALID_B64.match(b64_data).group() == b64_data: return from_b64(b64_data) else: raise XMPPError('not-acceptable') def set_data(self, value): self.xml.text = to_b64(value) def del_data(self): self.xml.text = '' class Close(ElementBase): name = 'close' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_close' interfaces = {'sid'} slixmpp-1.2.2/slixmpp/plugins/xep_0325/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0325/control.py0000644000175000001440000005463113004224717022661 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import time from slixmpp import asyncio from functools import partial from slixmpp.xmlstream import JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0325 import stanza from slixmpp.plugins.xep_0325.stanza import Control log = logging.getLogger(__name__) class XEP_0325(BasePlugin): """ XEP-0325: IoT Control Actuators are devices in sensor networks that can be controlled through the network and act with the outside world. In sensor networks and Internet of Things applications, actuators make it possible to automate real-world processes. This plugin implements a mechanism whereby actuators can be controlled in XMPP-based sensor networks, making it possible to integrate sensors and actuators of different brands, makes and models into larger Internet of Things applications. Also see Events: Sensor side ----------- Control Event:DirectSet -- Received a control message Control Event:SetReq -- Received a control request Client side ----------- Control Event:SetResponse -- Received a response to a control request, type result Control Event:SetResponseError -- Received a response to a control request, type error Attributes: sessions -- A dictionary or equivalent backend mapping session IDs to dictionaries containing data relevant to a request's session. This dictionary is used both by the client and sensor side. On client side, seqnr is used as key, while on sensor side, a session_id is used as key. This ensures that the two will not collide, so one instance can be both client and sensor. Sensor side ----------- nodes -- A dictionary mapping sensor nodes that are serviced through this XMPP instance to their device handlers ("drivers"). Client side ----------- last_seqnr -- The last used sequence number (integer). One sequence of communication (e.g. -->request, <--accept, <--fields) between client and sensor is identified by a unique sequence number (unique between the client/sensor pair) Methods: plugin_init -- Overrides BasePlugin.plugin_init post_init -- Overrides BasePlugin.post_init plugin_end -- Overrides BasePlugin.plugin_end Sensor side ----------- register_node -- Register a sensor as available from this XMPP instance. Client side ----------- set_request -- Initiates a control request to modify data in sensor(s). Non-blocking, a callback function will be called when the sensor has responded. set_command -- Initiates a control command to modify data in sensor(s). Non-blocking. The sensor(s) will not respond regardless of the result of the command, so no callback is made. """ name = 'xep_0325' description = 'XEP-0325 Internet of Things - Control' dependencies = {'xep_0030'} stanza = stanza default_config = { # 'session_db': None } def plugin_init(self): """ Start the XEP-0325 plugin """ self.xmpp.register_handler( Callback('Control Event:DirectSet', StanzaPath('message/set'), self._handle_direct_set)) self.xmpp.register_handler( Callback('Control Event:SetReq', StanzaPath('iq@type=set/set'), self._handle_set_req)) self.xmpp.register_handler( Callback('Control Event:SetResponse', StanzaPath('iq@type=result/setResponse'), self._handle_set_response)) self.xmpp.register_handler( Callback('Control Event:SetResponseError', StanzaPath('iq@type=error/setResponse'), self._handle_set_response)) # Server side dicts self.nodes = {} self.sessions = {} self.last_seqnr = 0 ## For testning only self.test_authenticated_from = "" def post_init(self): """ Init complete. Register our features in Service discovery. """ BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature(Control.namespace) self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) def _new_session(self): """ Return a new session ID. """ return str(time.time()) + '-' + self.xmpp.new_id() def plugin_end(self): """ Stop the XEP-0325 plugin """ self.sessions.clear() self.xmpp.remove_handler('Control Event:DirectSet') self.xmpp.remove_handler('Control Event:SetReq') self.xmpp.remove_handler('Control Event:SetResponse') self.xmpp.remove_handler('Control Event:SetResponseError') self.xmpp['xep_0030'].del_feature(feature=Control.namespace) self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) # ================================================================= # Sensor side (data provider) API def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): """ Register a sensor/device as available for control requests/commands through this XMPP instance. The device object may by any custom implementation to support specific devices, but it must implement the functions: has_control_field set_control_fields according to the interfaces shown in the example device.py file. Arguments: nodeId -- The identifier for the device device -- The device object commTimeout -- Time in seconds to wait between each callback from device during a data readout. Float. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ self.nodes[nodeId] = {"device": device, "commTimeout": commTimeout, "sourceId": sourceId, "cacheType": cacheType} def _set_authenticated(self, auth=''): """ Internal testing function """ self.test_authenticated_from = auth def _get_new_seqnr(self): """ Returns a unique sequence number (unique across threads) """ self.last_seqnr = self.last_seqnr + 1 return str(self.last_seqnr) def _handle_set_req(self, iq): """ Event handler for reception of an Iq with set req - this is a control request. Verifies that - all the requested nodes are available (if no nodes are specified in the request, assume all nodes) - all the control fields are available from all requested nodes (if no nodes are specified in the request, assume all nodes) If the request passes verification, the control request is passed to the devices (in a separate thread). If the verification fails, a setResponse with error indication is sent. """ error_msg = '' req_ok = True missing_node = None missing_field = None # Authentication if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: # Invalid authentication req_ok = False error_msg = "Access denied" # Nodes if len(iq['set']['nodes']) > 0: for n in iq['set']['nodes']: if not n['nodeId'] in self.nodes: req_ok = False missing_node = n['nodeId'] error_msg = "Invalid nodeId " + n['nodeId'] process_nodes = [n['nodeId'] for n in iq['set']['nodes']] else: process_nodes = self.nodes.keys() # Fields - for control we need to find all in all devices, otherwise we reject process_fields = [] if len(iq['set']['datas']) > 0: for f in iq['set']['datas']: for node in self.nodes: if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): req_ok = False missing_field = f['name'] error_msg = "Invalid field " + f['name'] break process_fields = [(f['name'], f._get_typename(), f['value']) for f in iq['set']['datas']] if req_ok: session = self._new_session() self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": iq['id']} self.sessions[session]["commTimers"] = {} self.sessions[session]["nodeDone"] = {} # Flag that a reply is exected when we are done self.sessions[session]["reply"] = True self.sessions[session]["node_list"] = process_nodes self._node_request(session, process_fields) else: iq = iq.reply() iq['type'] = 'error' iq['setResponse']['responseCode'] = "NotFound" if missing_node is not None: iq['setResponse'].add_node(missing_node) if missing_field is not None: iq['setResponse'].add_data(missing_field) iq['setResponse']['error']['var'] = "Output" iq['setResponse']['error']['text'] = error_msg iq.send() def _handle_direct_set(self, msg): """ Event handler for reception of a Message with set command - this is a direct control command. Verifies that - all the requested nodes are available (if no nodes are specified in the request, assume all nodes) - all the control fields are available from all requested nodes (if no nodes are specified in the request, assume all nodes) If the request passes verification, the control request is passed to the devices (in a separate thread). If the verification fails, do nothing. """ req_ok = True # Nodes if len(msg['set']['nodes']) > 0: for n in msg['set']['nodes']: if not n['nodeId'] in self.nodes: req_ok = False error_msg = "Invalid nodeId " + n['nodeId'] process_nodes = [n['nodeId'] for n in msg['set']['nodes']] else: process_nodes = self.nodes.keys() # Fields - for control we need to find all in all devices, otherwise we reject process_fields = [] if len(msg['set']['datas']) > 0: for f in msg['set']['datas']: for node in self.nodes: if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): req_ok = False missing_field = f['name'] error_msg = "Invalid field " + f['name'] break process_fields = [(f['name'], f._get_typename(), f['value']) for f in msg['set']['datas']] if req_ok: session = self._new_session() self.sessions[session] = {"from": msg['from'], "to": msg['to']} self.sessions[session]["commTimers"] = {} self.sessions[session]["nodeDone"] = {} self.sessions[session]["reply"] = False self.sessions[session]["node_list"] = process_nodes self._node_request(session, process_fields) def _node_request(self, session, process_fields): """ Helper function to handle the device control in a separate thread. Arguments: session -- The request session id process_fields -- The fields to set in the devices. List of tuple format: (name, datatype, value) """ for node in self.sessions[session]["node_list"]: self.sessions[session]["nodeDone"][node] = False for node in self.sessions[session]["node_list"]: timer = self.xmpp.loop.call_later(self.nodes[node]['commTimeout'], partial(self._event_comm_timeout, args=(session, node))) self.sessions[session]["commTimers"][node] = timer self.nodes[node]['device'].set_control_fields(process_fields, session=session, callback=self._device_set_command_callback) def _event_comm_timeout(self, session, nodeId): """ Triggered if any of the control operations timeout. Stop communicating with the failing device. If the control command was an Iq request, sends a failure message back to the client. Arguments: session -- The request session id nodeId -- The id of the device which timed out """ if self.sessions[session]["reply"]: # Reply is exected when we are done iq = self.xmpp.Iq() iq['from'] = self.sessions[session]['to'] iq['to'] = self.sessions[session]['from'] iq['type'] = "error" iq['id'] = self.sessions[session]['seqnr'] iq['setResponse']['responseCode'] = "OtherError" iq['setResponse'].add_node(nodeId) iq['setResponse']['error']['var'] = "Output" iq['setResponse']['error']['text'] = "Timeout." iq.send() ## TODO - should we send one timeout per node?? # Drop communication with this device and check if we are done self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): # The session is complete, delete it del self.sessions[session] def _all_nodes_done(self, session): """ Checks wheter all devices are done replying to the control command. Arguments: session -- The request session id """ for n in self.sessions[session]["nodeDone"]: if not self.sessions[session]["nodeDone"][n]: return False return True def _device_set_command_callback(self, session, nodeId, result, error_field=None, error_msg=None): """ Callback function called by the devices when the control command is complete or failed. If needed, composes a message with the result and sends it back to the client. Arguments: session -- The request session id nodeId -- The device id which initiated the callback result -- The current result status of the control command. Valid values are: "error" - Set fields failed. "ok" - All fields were set. error_field -- [optional] Only applies when result == "error" The field name that failed (usually means it is missing) error_msg -- [optional] Only applies when result == "error". Error details when a request failed. """ if not session in self.sessions: # This can happend if a session was deleted, like in a timeout. Just drop the data. return if result == "error": self.sessions[session]["commTimers"][nodeId].cancel() if self.sessions[session]["reply"]: # Reply is exected when we are done iq = self.xmpp.Iq() iq['from'] = self.sessions[session]['to'] iq['to'] = self.sessions[session]['from'] iq['type'] = "error" iq['id'] = self.sessions[session]['seqnr'] iq['setResponse']['responseCode'] = "OtherError" iq['setResponse'].add_node(nodeId) if error_field is not None: iq['setResponse'].add_data(error_field) iq['setResponse']['error']['var'] = error_field iq['setResponse']['error']['text'] = error_msg iq.send() # Drop communication with this device and check if we are done self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): # The session is complete, delete it del self.sessions[session] else: self.sessions[session]["commTimers"][nodeId].cancel() self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): if self.sessions[session]["reply"]: # Reply is exected when we are done iq = self.xmpp.Iq() iq['from'] = self.sessions[session]['to'] iq['to'] = self.sessions[session]['from'] iq['type'] = "result" iq['id'] = self.sessions[session]['seqnr'] iq['setResponse']['responseCode'] = "OK" iq.send() # The session is complete, delete it del self.sessions[session] # ================================================================= # Client side (data controller) API def set_request(self, from_jid, to_jid, callback, fields, nodeIds=None): """ Called on the client side to initiade a control request. Composes a message with the request and sends it to the device(s). Does not block, the callback will be called when the device(s) has responded. Arguments: from_jid -- The jid of the requester to_jid -- The jid of the device(s) callback -- The callback function to call when data is availble. The callback function must support the following arguments: from_jid -- The jid of the responding device(s) result -- The result of the control request. Valid values are: "OK" - Control request completed successfully "NotFound" - One or more nodes or fields are missing "InsufficientPrivileges" - Not authorized. "Locked" - Field(s) is locked and cannot be changed at the moment. "NotImplemented" - Request feature not implemented. "FormError" - Error while setting with a form (not implemented). "OtherError" - Indicates other types of errors, such as timeout. Details in the error_msg. nodeId -- [optional] Only applicable when result == "error" List of node Ids of failing device(s). fields -- [optional] Only applicable when result == "error" List of fields that failed.[optional] Mandatory when result == "rejected" or "failure". error_msg -- Details about why the request failed. fields -- Fields to set. List of tuple format: (name, typename, value). nodeIds -- [optional] Limits the request to the node Ids in this list. """ iq = self.xmpp.Iq() iq['from'] = from_jid iq['to'] = to_jid seqnr = self._get_new_seqnr() iq['id'] = seqnr iq['type'] = "set" if nodeIds is not None: for nodeId in nodeIds: iq['set'].add_node(nodeId) if fields is not None: for name, typename, value in fields: iq['set'].add_data(name=name, typename=typename, value=value) self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "callback": callback} iq.send() def set_command(self, from_jid, to_jid, fields, nodeIds=None): """ Called on the client side to initiade a control command. Composes a message with the set commandand sends it to the device(s). Does not block. Device(s) will not respond, regardless of result. Arguments: from_jid -- The jid of the requester to_jid -- The jid of the device(s) fields -- Fields to set. List of tuple format: (name, typename, value). nodeIds -- [optional] Limits the request to the node Ids in this list. """ msg = self.xmpp.Message() msg['from'] = from_jid msg['to'] = to_jid msg['type'] = "set" if nodeIds is not None: for nodeId in nodeIds: msg['set'].add_node(nodeId) if fields is not None: for name, typename, value in fields: msg['set'].add_data(name, typename, value) # We won't get any reply, so don't create a session msg.send() def _handle_set_response(self, iq): """ Received response from device(s) """ #print("ooh") seqnr = iq['id'] from_jid = str(iq['from']) result = iq['setResponse']['responseCode'] nodeIds = [n['name'] for n in iq['setResponse']['nodes']] fields = [f['name'] for f in iq['setResponse']['datas']] error_msg = None if not iq['setResponse'].xml.find('error') is None and not iq['setResponse']['error']['text'] == "": error_msg = iq['setResponse']['error']['text'] callback = self.sessions[seqnr]["callback"] callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg) slixmpp-1.2.2/slixmpp/plugins/xep_0325/device.py0000644000175000001440000001065212424504520022431 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime class Device(object): """ Example implementation of a device control object. The device object may by any custom implementation to support specific devices, but it must implement the functions: has_control_field set_control_fields """ def __init__(self, nodeId): self.nodeId = nodeId self.control_fields = {} def has_control_field(self, field, typename): """ Returns true if the supplied field name exists and the type matches for control in this device. Arguments: field -- The field name typename -- The expected type """ if field in self.control_fields and self.control_fields[field]["type"] == typename: return True return False def set_control_fields(self, fields, session, callback): """ Starts a control setting procedure. Verifies the fields, sets the data and (if needed) and calls the callback. Arguments: fields -- List of control fields in tuple format: (name, typename, value) session -- Session id, only used in the callback as identifier callback -- Callback function to call when control set is complete. The callback function must support the following arguments: session -- Session id, as supplied in the request_fields call nodeId -- Identifier for this device result -- The current result status of the readout. Valid values are: "error" - Set fields failed. "ok" - All fields were set. error_field -- [optional] Only applies when result == "error" The field name that failed (usually means it is missing) error_msg -- [optional] Only applies when result == "error". Error details when a request failed. """ if len(fields) > 0: # Check availiability for name, typename, value in fields: if not self.has_control_field(name, typename): self._send_control_reject(session, name, "NotFound", callback) return False for name, typename, value in fields: self._set_field_value(name, value) callback(session, result="ok", nodeId=self.nodeId) return True def _send_control_reject(self, session, field, message, callback): """ Sends a reject to the caller Arguments: session -- Session id, see definition in set_control_fields function callback -- Callback function, see definition in set_control_fields function """ callback(session, result="error", nodeId=self.nodeId, error_field=field, error_msg=message) def _add_control_field(self, name, typename, value): """ Adds a control field to the device Arguments: name -- Name of the field typename -- Type of the field, one of: (boolean, color, string, date, dateTime, double, duration, int, long, time) value -- Field value """ self.control_fields[name] = {"type": typename, "value": value} def _set_field_value(self, name, value): """ Set the value of a control field Arguments: name -- Name of the field value -- New value for the field """ if name in self.control_fields: self.control_fields[name]["value"] = value def _get_field_value(self, name): """ Get the value of a control field. Only used for unit testing. Arguments: name -- Name of the field """ if name in self.control_fields: return self.control_fields[name]["value"] return None slixmpp-1.2.2/slixmpp/plugins/xep_0325/__init__.py0000644000175000001440000000101412424504520022721 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0325.control import XEP_0325 from slixmpp.plugins.xep_0325 import stanza register_plugin(XEP_0325) xep_0325=XEP_0325 slixmpp-1.2.2/slixmpp/plugins/xep_0325/stanza/0000755000175000001440000000000013014656513022122 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0325/stanza/control.py0000644000175000001440000003751113004224717024157 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Iq, Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from re import match class Control(ElementBase): """ Placeholder for the namespace, not used as a stanza """ namespace = 'urn:xmpp:iot:control' name = 'control' plugin_attrib = name interfaces = set(tuple()) class ControlSet(ElementBase): namespace = 'urn:xmpp:iot:control' name = 'set' plugin_attrib = name interfaces = {'nodes','datas'} def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._nodes = set() self._datas = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._nodes = {node['nodeId'] for node in self['nodes']} self._datas = {data['name'] for data in self['datas']} def add_node(self, nodeId, sourceId=None, cacheType=None): """ Add a new node element. Each item is required to have a nodeId, but may also specify a sourceId value and cacheType. Arguments: nodeId -- The ID for the node. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ if nodeId not in self._nodes: self._nodes.add((nodeId)) node = RequestNode(parent=self) node['nodeId'] = nodeId node['sourceId'] = sourceId node['cacheType'] = cacheType self.iterables.append(node) return node return None def del_node(self, nodeId): """ Remove a single node. Arguments: nodeId -- Node ID of the item to remove. """ if nodeId in self._nodes: nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: if node['nodeId'] == nodeId: self.xml.remove(node.xml) self.iterables.remove(node) return True return False def get_nodes(self): """Return all nodes.""" nodes = [] for node in self['substanzas']: if isinstance(node, RequestNode): nodes.append(node) return nodes def set_nodes(self, nodes): """ Set or replace all nodes. The given nodes must be in a list or set where each item is a tuple of the form: (nodeId, sourceId, cacheType) Arguments: nodes -- A series of nodes in tuple format. """ self.del_nodes() for node in nodes: if isinstance(node, RequestNode): self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) else: nodeId, sourceId, cacheType = node self.add_node(nodeId, sourceId, cacheType) def del_nodes(self): """Remove all nodes.""" self._nodes = set() nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: self.xml.remove(node.xml) self.iterables.remove(node) def add_data(self, name, typename, value): """ Add a new data element. Arguments: name -- The name of the data element typename -- The type of data element (boolean, color, string, date, dateTime, double, duration, int, long, time) value -- The value of the data element """ if name not in self._datas: dataObj = None if typename == "boolean": dataObj = BooleanParameter(parent=self) elif typename == "color": dataObj = ColorParameter(parent=self) elif typename == "string": dataObj = StringParameter(parent=self) elif typename == "date": dataObj = DateParameter(parent=self) elif typename == "dateTime": dataObj = DateTimeParameter(parent=self) elif typename == "double": dataObj = DoubleParameter(parent=self) elif typename == "duration": dataObj = DurationParameter(parent=self) elif typename == "int": dataObj = IntParameter(parent=self) elif typename == "long": dataObj = LongParameter(parent=self) elif typename == "time": dataObj = TimeParameter(parent=self) dataObj['name'] = name dataObj['value'] = value self._datas.add(name) self.iterables.append(dataObj) return dataObj return None def del_data(self, name): """ Remove a single data element. Arguments: data_name -- The data element name to remove. """ if name in self._datas: datas = [i for i in self.iterables if isinstance(i, BaseParameter)] for data in datas: if data['name'] == name: self.xml.remove(data.xml) self.iterables.remove(data) return True return False def get_datas(self): """ Return all data elements. """ datas = [] for data in self['substanzas']: if isinstance(data, BaseParameter): datas.append(data) return datas def set_datas(self, datas): """ Set or replace all data elements. The given elements must be in a list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) Arguments: datas -- A series of data elements. """ self.del_datas() for data in datas: self.add_data(name=data['name'], typename=data._get_typename(), value=data['value']) def del_datas(self): """Remove all data elements.""" self._datas = set() datas = [i for i in self.iterables if isinstance(i, BaseParameter)] for data in datas: self.xml.remove(data.xml) self.iterables.remove(data) class RequestNode(ElementBase): """ Node element in a request """ namespace = 'urn:xmpp:iot:control' name = 'node' plugin_attrib = name interfaces = {'nodeId','sourceId','cacheType'} class ControlSetResponse(ElementBase): namespace = 'urn:xmpp:iot:control' name = 'setResponse' plugin_attrib = name interfaces = {'responseCode'} def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._nodes = set() self._datas = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._nodes = {node['nodeId'] for node in self['nodes']} self._datas = {data['name'] for data in self['datas']} def add_node(self, nodeId, sourceId=None, cacheType=None): """ Add a new node element. Each item is required to have a nodeId, but may also specify a sourceId value and cacheType. Arguments: nodeId -- The ID for the node. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ if nodeId not in self._nodes: self._nodes.add(nodeId) node = RequestNode(parent=self) node['nodeId'] = nodeId node['sourceId'] = sourceId node['cacheType'] = cacheType self.iterables.append(node) return node return None def del_node(self, nodeId): """ Remove a single node. Arguments: nodeId -- Node ID of the item to remove. """ if nodeId in self._nodes: nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: if node['nodeId'] == nodeId: self.xml.remove(node.xml) self.iterables.remove(node) return True return False def get_nodes(self): """Return all nodes.""" nodes = [] for node in self['substanzas']: if isinstance(node, RequestNode): nodes.append(node) return nodes def set_nodes(self, nodes): """ Set or replace all nodes. The given nodes must be in a list or set where each item is a tuple of the form: (nodeId, sourceId, cacheType) Arguments: nodes -- A series of nodes in tuple format. """ self.del_nodes() for node in nodes: if isinstance(node, RequestNode): self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) else: nodeId, sourceId, cacheType = node self.add_node(nodeId, sourceId, cacheType) def del_nodes(self): """Remove all nodes.""" self._nodes = set() nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: self.xml.remove(node.xml) self.iterables.remove(node) def add_data(self, name): """ Add a new ResponseParameter element. Arguments: name -- Name of the parameter """ if name not in self._datas: self._datas.add(name) data = ResponseParameter(parent=self) data['name'] = name self.iterables.append(data) return data return None def del_data(self, name): """ Remove a single ResponseParameter element. Arguments: name -- The data element name to remove. """ if name in self._datas: datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] for data in datas: if data['name'] == name: self.xml.remove(data.xml) self.iterables.remove(data) return True return False def get_datas(self): """ Return all ResponseParameter elements. """ datas = set() for data in self['substanzas']: if isinstance(data, ResponseParameter): datas.add(data) return datas def set_datas(self, datas): """ Set or replace all data elements. The given elements must be in a list or set of ResponseParameter elements Arguments: datas -- A series of data element names. """ self.del_datas() for data in datas: self.add_data(name=data['name']) def del_datas(self): """Remove all ResponseParameter elements.""" self._datas = set() datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] for data in datas: self.xml.remove(data.xml) self.iterables.remove(data) class Error(ElementBase): namespace = 'urn:xmpp:iot:control' name = 'error' plugin_attrib = name interfaces = {'var','text'} def get_text(self): """Return then contents inside the XML tag.""" return self.xml.text def set_text(self, value): """Set then contents inside the XML tag. Arguments: value -- string """ self.xml.text = value return self def del_text(self): """Remove the contents inside the XML tag.""" self.xml.text = "" return self class ResponseParameter(ElementBase): """ Parameter element in ControlSetResponse. """ namespace = 'urn:xmpp:iot:control' name = 'parameter' plugin_attrib = name interfaces = {'name'} class BaseParameter(ElementBase): """ Parameter element in SetCommand. This is a base class, all instances of parameters added to SetCommand must be of types: BooleanParameter ColorParameter StringParameter DateParameter DateTimeParameter DoubleParameter DurationParameter IntParameter LongParameter TimeParameter """ namespace = 'urn:xmpp:iot:control' name = 'baseParameter' plugin_attrib = name interfaces = {'name','value'} def _get_typename(self): return self.name class BooleanParameter(BaseParameter): """ Field data of type boolean. Note that the value is expressed as a string. """ name = 'boolean' plugin_attrib = name class ColorParameter(BaseParameter): """ Field data of type color. Note that the value is expressed as a string. """ name = 'color' plugin_attrib = name class StringParameter(BaseParameter): """ Field data of type string. """ name = 'string' plugin_attrib = name class DateParameter(BaseParameter): """ Field data of type date. Note that the value is expressed as a string. """ name = 'date' plugin_attrib = name class DateTimeParameter(BaseParameter): """ Field data of type dateTime. Note that the value is expressed as a string. """ name = 'dateTime' plugin_attrib = name class DoubleParameter(BaseParameter): """ Field data of type double. Note that the value is expressed as a string. """ name = 'double' plugin_attrib = name class DurationParameter(BaseParameter): """ Field data of type duration. Note that the value is expressed as a string. """ name = 'duration' plugin_attrib = name class IntParameter(BaseParameter): """ Field data of type int. Note that the value is expressed as a string. """ name = 'int' plugin_attrib = name class LongParameter(BaseParameter): """ Field data of type long (64-bit int). Note that the value is expressed as a string. """ name = 'long' plugin_attrib = name class TimeParameter(BaseParameter): """ Field data of type time. Note that the value is expressed as a string. """ name = 'time' plugin_attrib = name register_stanza_plugin(Iq, ControlSet) register_stanza_plugin(Message, ControlSet) register_stanza_plugin(ControlSet, RequestNode, iterable=True) register_stanza_plugin(ControlSet, BooleanParameter, iterable=True) register_stanza_plugin(ControlSet, ColorParameter, iterable=True) register_stanza_plugin(ControlSet, StringParameter, iterable=True) register_stanza_plugin(ControlSet, DateParameter, iterable=True) register_stanza_plugin(ControlSet, DateTimeParameter, iterable=True) register_stanza_plugin(ControlSet, DoubleParameter, iterable=True) register_stanza_plugin(ControlSet, DurationParameter, iterable=True) register_stanza_plugin(ControlSet, IntParameter, iterable=True) register_stanza_plugin(ControlSet, LongParameter, iterable=True) register_stanza_plugin(ControlSet, TimeParameter, iterable=True) register_stanza_plugin(Iq, ControlSetResponse) register_stanza_plugin(ControlSetResponse, Error) register_stanza_plugin(ControlSetResponse, RequestNode, iterable=True) register_stanza_plugin(ControlSetResponse, ResponseParameter, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0325/stanza/__init__.py0000644000175000001440000000060112424504520024222 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0325.stanza.control import * slixmpp-1.2.2/slixmpp/plugins/xep_0325/stanza/base.py0000644000175000001440000000056112424504520023402 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ET pass slixmpp-1.2.2/slixmpp/plugins/xep_0332/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0332/__init__.py0000644000175000001440000000070412603534742022734 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0332 import stanza from slixmpp.plugins.xep_0332.http import XEP_0332 register_plugin(XEP_0332) slixmpp-1.2.2/slixmpp/plugins/xep_0332/stanza/0000755000175000001440000000000013014656513022120 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0332/stanza/data.py0000644000175000001440000000137113004224717023401 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class HTTPData(ElementBase): """ The data element. """ name = 'data' namespace = 'urn:xmpp:http' interfaces = {'data'} plugin_attrib = 'data' is_extension = True def get_data(self, encoding='text'): data = self._get_sub_text(encoding, None) return str(data) if data is not None else data def set_data(self, data, encoding='text'): self._set_sub_text(encoding, text=data) slixmpp-1.2.2/slixmpp/plugins/xep_0332/stanza/__init__.py0000644000175000001440000000072312603534742024235 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0332.stanza.request import HTTPRequest from slixmpp.plugins.xep_0332.stanza.response import HTTPResponse from slixmpp.plugins.xep_0332.stanza.data import HTTPData slixmpp-1.2.2/slixmpp/plugins/xep_0332/stanza/response.py0000644000175000001440000000410213004224717024321 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class HTTPResponse(ElementBase): """ When the HTTP Server responds, it does so by sending an `iq` stanza response (type=`result`) back to the client containing the `resp` element. Since response are asynchronous, and since multiple requests may be active at the same time, responses may be returned in a different order than the in which the original requests were made. Examples:
Fri, 03 May 2013 16:39:54GMT-4
Clayster
text/turtle
...
Close
...
""" name = 'response' namespace = 'urn:xmpp:http' interfaces = {'code', 'message', 'version'} plugin_attrib = 'http-resp' def get_code(self): code = self._get_attr('statusCode', None) return int(code) if code is not None else code def set_code(self, code): self._set_attr('statusCode', str(code)) def get_message(self): return self._get_attr('statusMessage', '') def set_message(self, message): self._set_attr('statusMessage', message) def set_version(self, version='1.1'): self._set_attr('version', version) slixmpp-1.2.2/slixmpp/plugins/xep_0332/stanza/request.py0000644000175000001440000000406413004224717024162 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class HTTPRequest(ElementBase): """ All HTTP communication is done using the `Request`/`Response` paradigm. Each HTTP Request is made sending an `iq` stanza containing a `req` element to the server. Each `iq` stanza sent is of type `set`. Examples:
b.com
b.com
text/html
...
...
""" name = 'request' namespace = 'urn:xmpp:http' interfaces = {'method', 'resource', 'version'} plugin_attrib = 'http-req' def get_method(self): return self._get_attr('method', None) def set_method(self, method): self._set_attr('method', method) def get_resource(self): return self._get_attr('resource', None) def set_resource(self, resource): self._set_attr('resource', resource) def get_version(self): return self._get_attr('version', None) def set_version(self, version='1.1'): self._set_attr('version', version) slixmpp-1.2.2/slixmpp/plugins/xep_0332/http.py0000644000175000001440000001315613004224717022153 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of HTTP over XMPP transport http://xmpp.org/extensions/xep-0332.html Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com This file is part of slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0332.stanza import ( HTTPRequest, HTTPResponse, HTTPData ) from slixmpp.plugins.xep_0131.stanza import Headers log = logging.getLogger(__name__) class XEP_0332(BasePlugin): """ XEP-0332: HTTP over XMPP transport """ name = 'xep_0332' description = 'XEP-0332: HTTP over XMPP transport' #: xep_0047 not included. #: xep_0001, 0137 and 0166 are missing dependencies = {'xep_0030', 'xep_0131'} #: TODO: Do we really need to mention the supported_headers?! default_config = { 'supported_headers': { 'Content-Length', 'Transfer-Encoding', 'DateTime', 'Accept-Charset', 'Location', 'Content-ID', 'Description', 'Content-Language', 'Content-Transfer-Encoding', 'Timestamp', 'Expires', 'User-Agent', 'Host', 'Proxy-Authorization', 'Date', 'WWW-Authenticate', 'Accept-Encoding', 'Server', 'Error-Info', 'Identifier', 'Content-Location', 'Content-Encoding', 'Distribute', 'Accept', 'Proxy-Authenticate', 'ETag', 'Expect', 'Content-Type' } } def plugin_init(self): self.xmpp.register_handler( Callback( 'HTTP Request', StanzaPath('iq/http-req'), self._handle_request ) ) self.xmpp.register_handler( Callback( 'HTTP Response', StanzaPath('iq/http-resp'), self._handle_response ) ) register_stanza_plugin(Iq, HTTPRequest, iterable=True) register_stanza_plugin(Iq, HTTPResponse, iterable=True) register_stanza_plugin(HTTPRequest, Headers, iterable=True) register_stanza_plugin(HTTPRequest, HTTPData, iterable=True) register_stanza_plugin(HTTPResponse, Headers, iterable=True) register_stanza_plugin(HTTPResponse, HTTPData, iterable=True) # TODO: Should we register any api's here? self.api.register() def plugin_end(self): self.xmpp.remove_handler('HTTP Request') self.xmpp.remove_handler('HTTP Response') self.xmpp['xep_0030'].del_feature('urn:xmpp:http') for header in self.supported_headers: self.xmpp['xep_0030'].del_feature( feature='%s#%s' % (Headers.namespace, header) ) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:http') for header in self.supported_headers: self.xmpp['xep_0030'].add_feature( '%s#%s' % (Headers.namespace, header) ) # TODO: Do we need to add the supported headers to xep_0131? # self.xmpp['xep_0131'].supported_headers.add(header) def _handle_request(self, iq): self.xmpp.event('http_request', iq) def _handle_response(self, iq): self.xmpp.event('http_response', iq) def send_request(self, to=None, method=None, resource=None, headers=None, data=None, **kwargs): iq = self.xmpp.Iq() iq['from'] = self.xmpp.boundjid iq['to'] = to iq['type'] = 'set' iq['http-req']['headers'] = headers iq['http-req']['method'] = method iq['http-req']['resource'] = resource iq['http-req']['version'] = '1.1' # TODO: set this implicitly if 'id' in kwargs: iq['id'] = kwargs["id"] if data is not None: iq['http-req']['data'] = data return iq.send( timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), callback=kwargs.get('callback', None), timeout_callback=kwargs.get('timeout_callback', None) ) def send_response(self, to=None, code=None, message=None, headers=None, data=None, **kwargs): iq = self.xmpp.Iq() iq['from'] = self.xmpp.boundjid iq['to'] = to iq['type'] = 'result' iq['http-resp']['headers'] = headers iq['http-resp']['code'] = code iq['http-resp']['message'] = message iq['http-resp']['version'] = '1.1' # TODO: set this implicitly if 'id' in kwargs: iq['id'] = kwargs["id"] if data is not None: iq['http-resp']['data'] = data return iq.send( timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), callback=kwargs.get('callback', None), timeout_callback=kwargs.get('timeout_callback', None) ) def send_error(self, to=None, ecode='500', etype='wait', econd='internal-server-error', **kwargs): iq = self.xmpp.Iq() iq['from'] = self.xmpp.boundjid iq['to'] = to iq['type'] = 'error' iq['error']['code'] = ecode iq['error']['type'] = etype iq['error']['condition'] = econd if 'id' in kwargs: iq['id'] = kwargs["id"] return iq.send( timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), callback=kwargs.get('callback', None), timeout_callback=kwargs.get('timeout_callback', None) ) slixmpp-1.2.2/slixmpp/plugins/xep_0106.py0000644000175000001440000000113113004224717021161 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins import BasePlugin, register_plugin class XEP_0106(BasePlugin): name = 'xep_0106' description = 'XEP-0106: JID Escaping' dependencies = {'xep_0030'} def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping') register_plugin(XEP_0106) slixmpp-1.2.2/slixmpp/plugins/xep_0071/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0071/__init__.py0000644000175000001440000000056512424504520022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0071.stanza import XHTML_IM from slixmpp.plugins.xep_0071.xhtml_im import XEP_0071 register_plugin(XEP_0071) slixmpp-1.2.2/slixmpp/plugins/xep_0071/xhtml_im.py0000644000175000001440000000143213004224717023007 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0071 import stanza, XHTML_IM class XEP_0071(BasePlugin): name = 'xep_0071' description = 'XEP-0071: XHTML-IM' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, XHTML_IM) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(feature=XHTML_IM.namespace) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=XHTML_IM.namespace) slixmpp-1.2.2/slixmpp/plugins/xep_0071/stanza.py0000644000175000001440000000542113004224717022470 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Message from slixmpp.util import unicode from collections import OrderedDict from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring XHTML_NS = 'http://www.w3.org/1999/xhtml' class XHTML_IM(ElementBase): namespace = 'http://jabber.org/protocol/xhtml-im' name = 'html' interfaces = {'body'} lang_interfaces = {'body'} plugin_attrib = name def set_body(self, content, lang=None): if lang is None: lang = self.get_lang() self.del_body(lang) if lang == '*': for sublang, subcontent in content.items(): self.set_body(subcontent, sublang) else: if isinstance(content, type(ET.Element('test'))): content = unicode(ET.tostring(content)) else: content = unicode(content) header = ' 1 values = {} if multi: values = msg.values del values['pubsub_event'] for item in msg['pubsub_event']['items']: event_name = self.node_event_map.get(node, None) event_type = 'publish' if item.name == 'retract': event_type = 'retract' if multi: condensed = self.xmpp.Message() condensed.values = values condensed['pubsub_event']['items']['node'] = node condensed['pubsub_event']['items'].append(item) self.xmpp.event('pubsub_%s' % event_type, msg) if event_name: self.xmpp.event('%s_%s' % (event_name, event_type), condensed) else: self.xmpp.event('pubsub_%s' % event_type, msg) if event_name: self.xmpp.event('%s_%s' % (event_name, event_type), msg) def _handle_event_purge(self, msg): """Raise events for node purge notifications.""" node = msg['pubsub_event']['purge']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_purge', msg) if event_name: self.xmpp.event('%s_purge' % event_name, msg) def _handle_event_delete(self, msg): """Raise events for node deletion notifications.""" node = msg['pubsub_event']['delete']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_delete', msg) if event_name: self.xmpp.event('%s_delete' % event_name, msg) def _handle_event_configuration(self, msg): """Raise events for node configuration notifications.""" node = msg['pubsub_event']['configuration']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_config', msg) if event_name: self.xmpp.event('%s_config' % event_name, msg) def _handle_event_subscription(self, msg): """Raise events for node subscription notifications.""" node = msg['pubsub_event']['subscription']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_subscription', msg) if event_name: self.xmpp.event('%s_subscription' % event_name, msg) def map_node_event(self, node, event_name): """ Map node names to events. When a pubsub event is received for the given node, raise the provided event. For example:: map_node_event('http://jabber.org/protocol/tune', 'user_tune') will produce the events 'user_tune_publish' and 'user_tune_retract' when the respective notifications are received from the node 'http://jabber.org/protocol/tune', among other events. Arguments: node -- The node name to map to an event. event_name -- The name of the event to raise when a notification from the given node is received. """ self.node_event_map[node] = event_name def create_node(self, jid, node, config=None, ntype=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Create and configure a new pubsub node. A server MAY use a different name for the node than the one provided, so be sure to check the result stanza for a server assigned name. If no configuration form is provided, the node will be created using the server's default configuration. To get the default configuration use get_node_config(). Arguments: jid -- The JID of the pubsub service. node -- Optional name of the node to create. If no name is provided, the server MAY generate a node ID for you. The server can also assign a different name than the one you provide; check the result stanza to see if the server assigned a name. config -- Optional XEP-0004 data form of configuration settings. ntype -- The type of node to create. Servers typically default to using 'leaf' if no type is provided. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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']['create']['node'] = node if config is not None: form_type = 'http://jabber.org/protocol/pubsub#node_config' if 'FORM_TYPE' in config['fields']: config.field['FORM_TYPE']['value'] = form_type else: config.add_field(var='FORM_TYPE', ftype='hidden', value=form_type) if ntype: if 'pubsub#node_type' in config['fields']: config.field['pubsub#node_type']['value'] = ntype else: config.add_field(var='pubsub#node_type', value=ntype) iq['pubsub']['configure'].append(config) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def subscribe(self, jid, node, bare=True, subscribee=None, options=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Subscribe to updates from a pubsub node. The rules for determining the JID that is subscribing to the node are: 1. If subscribee is given, use that as provided. 2. If ifrom was given, use the bare or full version based on bare. 3. Otherwise, use self.xmpp.boundjid based on bare. Arguments: jid -- The pubsub service JID. node -- The node to subscribe to. bare -- Indicates if the subscribee is a bare or full JID. Defaults to True for a bare JID. subscribee -- The JID that is subscribing to the node. options -- ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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']['subscribe']['node'] = node if subscribee is None: if ifrom: if bare: subscribee = JID(ifrom).bare else: subscribee = ifrom else: if bare: subscribee = self.xmpp.boundjid.bare else: subscribee = self.xmpp.boundjid iq['pubsub']['subscribe']['jid'] = subscribee if options is not None: iq['pubsub']['options'].append(options) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Unubscribe from updates from a pubsub node. The rules for determining the JID that is unsubscribing from the node are: 1. If subscribee is given, use that as provided. 2. If ifrom was given, use the bare or full version based on bare. 3. Otherwise, use self.xmpp.boundjid based on bare. Arguments: jid -- The pubsub service JID. node -- The node to unsubscribe from. subid -- The specific subscription, if multiple subscriptions exist for this JID/node combination. bare -- Indicates if the subscribee is a bare or full JID. Defaults to True for a bare JID. subscribee -- The JID that is unsubscribing from the node. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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']['unsubscribe']['node'] = node if subscribee is None: if ifrom: if bare: subscribee = JID(ifrom).bare else: subscribee = ifrom else: if bare: subscribee = self.xmpp.boundjid.bare else: subscribee = self.xmpp.boundjid iq['pubsub']['unsubscribe']['jid'] = subscribee iq['pubsub']['unsubscribe']['subid'] = subid return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_subscriptions(self, jid, node=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['subscriptions']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_affiliations(self, jid, node=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['affiliations']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') if user_jid is None: iq['pubsub']['default']['node'] = node else: iq['pubsub']['options']['node'] = node iq['pubsub']['options']['jid'] = user_jid return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def set_subscription_options(self, jid, node, user_jid, options, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['options']['node'] = node iq['pubsub']['options']['jid'] = user_jid iq['pubsub']['options'].append(options) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_node_config(self, jid, node=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Retrieve the configuration for a node, or the pubsub service's default configuration for new nodes. Arguments: jid -- The JID of the pubsub service. node -- The node to retrieve the configuration for. If None, the default configuration for new nodes will be requested. Defaults to None. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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='get') if node is None: iq['pubsub_owner']['default'] else: iq['pubsub_owner']['configure']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_node_subscriptions(self, jid, node, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Retrieve the subscriptions associated with a given node. Arguments: jid -- The JID of the pubsub service. node -- The node to retrieve subscriptions from. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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='get') iq['pubsub_owner']['subscriptions']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_node_affiliations(self, jid, node, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Retrieve the affiliations associated with a given node. Arguments: jid -- The JID of the pubsub service. node -- The node to retrieve affiliations from. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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='get') iq['pubsub_owner']['affiliations']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def delete_node(self, jid, node, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Delete a a pubsub node. Arguments: jid -- The JID of the pubsub service. node -- The node to delete. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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_owner']['delete']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def set_node_config(self, jid, node, config, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['configure']['node'] = node iq['pubsub_owner']['configure'].append(config) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def publish(self, jid, node, id=None, payload=None, options=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Add a new item to a node, or edit an existing item. 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. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.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 if id is not None: iq['pubsub']['publish']['item']['id'] = id if payload is not None: iq['pubsub']['publish']['item']['payload'] = payload iq['pubsub']['publish_options'] = options return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def retract(self, jid, node, id, notify=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Delete a single item from a node. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['retract']['node'] = node iq['pubsub']['retract']['notify'] = notify iq['pubsub']['retract']['item']['id'] = id return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def purge(self, jid, node, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Remove all items from a node. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['purge']['node'] = node return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_nodes(self, *args, **kwargs): """ Discover the nodes provided by a Pubsub service, using disco. """ return self.xmpp['xep_0030'].get_items(*args, **kwargs) def get_item(self, jid, node, item_id, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Retrieve the content of an individual item. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') item = stanza.Item() item['id'] = item_id iq['pubsub']['items']['node'] = node iq['pubsub']['items'].append(item) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_items(self, jid, node, item_ids=None, max_items=None, iterator=False, ifrom=None, timeout_callback=None, callback=None, timeout=None): """ Request the contents of a node's items. The desired items can be specified, or a query for the last few published items can be used. Pubsub services may use result set management for nodes with many items, so an iterator can be returned if needed. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['items']['node'] = node iq['pubsub']['items']['max_items'] = max_items if item_ids is not None: for item_id in item_ids: item = stanza.Item() item['id'] = item_id iq['pubsub']['items'].append(item) if iterator: return self.xmpp['xep_0059'].iterate(iq, 'pubsub') else: return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def get_item_ids(self, jid, node, ifrom=None, timeout_callback=None, callback=None, timeout=None, iterator=False): """ Retrieve the ItemIDs hosted by a given node, using disco. """ self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom, callback=callback, timeout=timeout, iterator=iterator, timeout_callback=timeout_callback) def modify_affiliations(self, jid, node, affiliations=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['affiliations']['node'] = node if affiliations is None: affiliations = [] for jid, affiliation in affiliations: aff = stanza.OwnerAffiliation() aff['jid'] = jid aff['affiliation'] = affiliation iq['pubsub_owner']['affiliations'].append(aff) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) def modify_subscriptions(self, jid, node, subscriptions=None, ifrom=None, timeout_callback=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['subscriptions']['node'] = node if subscriptions is None: subscriptions = [] for jid, subscription in subscriptions: sub = stanza.OwnerSubscription() sub['jid'] = jid sub['subscription'] = subscription iq['pubsub_owner']['subscriptions'].append(sub) return iq.send(callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0060/__init__.py0000644000175000001440000000055412770302340022725 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0060.pubsub import XEP_0060 from slixmpp.plugins.xep_0060 import stanza register_plugin(XEP_0060) slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/0000755000175000001440000000000013014656513022116 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/pubsub.py0000644000175000001440000001660213004224717023771 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Iq, Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins import xep_0004 from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting class Pubsub(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'pubsub' plugin_attrib = name interfaces = set(tuple()) class Affiliations(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliations' plugin_attrib = name interfaces = {'node'} class Affiliation(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliation' plugin_attrib = name interfaces = {'node', 'affiliation', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscription' plugin_attrib = name interfaces = {'jid', 'node', 'subscription', 'subid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscriptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscriptions' plugin_attrib = name interfaces = {'node'} class SubscribeOptions(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe-options' plugin_attrib = 'suboptions' interfaces = {'required'} class Item(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'item' plugin_attrib = name interfaces = {'id', 'payload'} def set_payload(self, value): del self['payload'] if isinstance(value, ElementBase): if value.tag_name() in self.plugin_tag_map: self.init_plugin(value.plugin_attrib, existing_xml=value.xml) self.xml.append(value.xml) else: self.xml.append(value) def get_payload(self): childs = list(self.xml) if len(childs) > 0: return childs[0] def del_payload(self): for child in self.xml: self.xml.remove(child) class Items(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'items' plugin_attrib = name interfaces = {'node', 'max_items'} def set_max_items(self, value): self._set_attr('max_items', str(value)) class Create(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'create' plugin_attrib = name interfaces = {'node'} class Default(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'default' plugin_attrib = name interfaces = {'node', 'type'} def get_type(self): t = self._get_attr('type') if not t: return 'leaf' return t class Publish(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish' plugin_attrib = name interfaces = {'node'} class Retract(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'retract' plugin_attrib = name interfaces = {'node', 'notify'} def get_notify(self): notify = self._get_attr('notify') if notify in ('0', 'false'): return False elif notify in ('1', 'true'): return True return None def set_notify(self, value): del self['notify'] if value is None: return elif value in (True, '1', 'true', 'True'): self._set_attr('notify', 'true') else: self._set_attr('notify', 'false') class Unsubscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'unsubscribe' plugin_attrib = name interfaces = {'node', 'jid', 'subid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe' plugin_attrib = name interfaces = {'node', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Configure(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'configure' plugin_attrib = name interfaces = {'node', 'type'} def getType(self): t = self._get_attr('type') if not t: t == 'leaf' return t class Options(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'options' plugin_attrib = name interfaces = {'jid', 'node', 'options'} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def get_options(self): config = self.xml.find('{jabber:x:data}x') form = xep_0004.Form(xml=config) return form def set_options(self, value): if isinstance(value, ElementBase): self.xml.append(value.xml) else: self.xml.append(value) return self def del_options(self): config = self.xml.find('{jabber:x:data}x') self.xml.remove(config) def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class PublishOptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish-options' plugin_attrib = 'publish_options' interfaces = {'publish_options'} is_extension = True def get_publish_options(self): config = self.xml.find('{jabber:x:data}x') if config is None: return None form = xep_0004.Form(xml=config) return form def set_publish_options(self, value): if value is None: self.del_publish_options() else: if isinstance(value, ElementBase): self.xml.append(value.xml) else: self.xml.append(value) return self def del_publish_options(self): config = self.xml.find('{jabber:x:data}x') if config is not None: self.xml.remove(config) self.parent().xml.remove(self.xml) register_stanza_plugin(Iq, Pubsub) register_stanza_plugin(Pubsub, Affiliations) register_stanza_plugin(Pubsub, Configure) register_stanza_plugin(Pubsub, Create) register_stanza_plugin(Pubsub, Default) register_stanza_plugin(Pubsub, Items) register_stanza_plugin(Pubsub, Options) register_stanza_plugin(Pubsub, Publish) register_stanza_plugin(Pubsub, PublishOptions) register_stanza_plugin(Pubsub, Retract) register_stanza_plugin(Pubsub, Subscribe) register_stanza_plugin(Pubsub, Subscription) register_stanza_plugin(Pubsub, Subscriptions) register_stanza_plugin(Pubsub, Unsubscribe) register_stanza_plugin(Affiliations, Affiliation, iterable=True) register_stanza_plugin(Configure, xep_0004.Form) register_stanza_plugin(Items, Item, iterable=True) register_stanza_plugin(Publish, Item, iterable=True) register_stanza_plugin(Retract, Item) register_stanza_plugin(Subscribe, Options) register_stanza_plugin(Subscription, SubscribeOptions) register_stanza_plugin(Subscriptions, Subscription, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/__init__.py0000644000175000001440000000062312424504520024222 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0060.stanza.pubsub import * from slixmpp.plugins.xep_0060.stanza.pubsub_owner import * from slixmpp.plugins.xep_0060.stanza.pubsub_event import * from slixmpp.plugins.xep_0060.stanza.pubsub_errors import * slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/pubsub_owner.py0000644000175000001440000000754013004224717025204 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting from slixmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation from slixmpp.plugins.xep_0060.stanza.pubsub import Configure, Subscriptions class PubsubOwner(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'pubsub' plugin_attrib = 'pubsub_owner' interfaces = set(tuple()) class DefaultConfig(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'default' plugin_attrib = name interfaces = {'node', 'config'} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def get_config(self): return self['form'] def set_config(self, value): del self['from'] self.append(value) return self class OwnerAffiliations(Affiliations): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'node'} def append(self, affiliation): if not isinstance(affiliation, OwnerAffiliation): raise TypeError self.xml.append(affiliation.xml) class OwnerAffiliation(Affiliation): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'affiliation', 'jid'} class OwnerConfigure(Configure): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'configure' plugin_attrib = name interfaces = {'node'} class OwnerDefault(OwnerConfigure): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'node'} class OwnerDelete(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'delete' plugin_attrib = name interfaces = {'node'} class OwnerPurge(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'purge' plugin_attrib = name interfaces = {'node'} class OwnerRedirect(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'redirect' plugin_attrib = name interfaces = {'node', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class OwnerSubscriptions(Subscriptions): name = 'subscriptions' namespace = 'http://jabber.org/protocol/pubsub#owner' plugin_attrib = name interfaces = {'node'} def append(self, subscription): if not isinstance(subscription, OwnerSubscription): raise TypeError self.xml.append(subscription.xml) class OwnerSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'subscription' plugin_attrib = name interfaces = {'jid', 'subscription'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Iq, PubsubOwner) register_stanza_plugin(PubsubOwner, DefaultConfig) register_stanza_plugin(PubsubOwner, OwnerAffiliations) register_stanza_plugin(PubsubOwner, OwnerConfigure) register_stanza_plugin(PubsubOwner, OwnerDefault) register_stanza_plugin(PubsubOwner, OwnerDelete) register_stanza_plugin(PubsubOwner, OwnerPurge) register_stanza_plugin(PubsubOwner, OwnerSubscriptions) register_stanza_plugin(DefaultConfig, Form) register_stanza_plugin(OwnerAffiliations, OwnerAffiliation, iterable=True) register_stanza_plugin(OwnerConfigure, Form) register_stanza_plugin(OwnerDefault, Form) register_stanza_plugin(OwnerDelete, OwnerRedirect) register_stanza_plugin(OwnerSubscriptions, OwnerSubscription, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/pubsub_event.py0000644000175000001440000001017313004224717025167 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins import xep_0082 class Event(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'event' plugin_attrib = 'pubsub_event' interfaces = set() class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'item' plugin_attrib = name interfaces = {'id', 'payload', 'node', 'publisher'} def set_payload(self, value): self.xml.append(value) def get_payload(self): childs = list(self.xml) if len(childs) > 0: return childs[0] def del_payload(self): for child in self.xml: self.xml.remove(child) class EventRetract(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'retract' plugin_attrib = name interfaces = {'id'} class EventItems(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'items' plugin_attrib = name interfaces = {'node'} class EventCollection(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'collection' plugin_attrib = name interfaces = {'node'} class EventAssociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'associate' plugin_attrib = name interfaces = {'node'} class EventDisassociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'disassociate' plugin_attrib = name interfaces = {'node'} class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'configuration' plugin_attrib = name interfaces = {'node'} class EventPurge(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'purge' plugin_attrib = name interfaces = {'node'} class EventDelete(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'delete' plugin_attrib = name interfaces = {'node', 'redirect'} def set_redirect(self, uri): del self['redirect'] redirect = ET.Element('{%s}redirect' % self.namespace) redirect.attrib['uri'] = uri self.xml.append(redirect) def get_redirect(self): redirect = self.xml.find('{%s}redirect' % self.namespace) if redirect is not None: return redirect.attrib.get('uri', '') return '' def del_redirect(self): redirect = self.xml.find('{%s}redirect' % self.namespace) if redirect is not None: self.xml.remove(redirect) class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'subscription' plugin_attrib = name interfaces = {'node', 'expiry', 'jid', 'subid', 'subscription'} def get_expiry(self): expiry = self._get_attr('expiry') if expiry.lower() == 'presence': return expiry return xep_0082.parse(expiry) def set_expiry(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('expiry', value) def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Message, Event) register_stanza_plugin(Event, EventCollection) register_stanza_plugin(Event, EventConfiguration) register_stanza_plugin(Event, EventPurge) register_stanza_plugin(Event, EventDelete) register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventSubscription) register_stanza_plugin(EventCollection, EventAssociate) register_stanza_plugin(EventCollection, EventDisassociate) register_stanza_plugin(EventConfiguration, Form) register_stanza_plugin(EventItems, EventItem, iterable=True) register_stanza_plugin(EventItems, EventRetract, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/base.py0000644000175000001440000000142313004224717023376 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ET class OptionalSetting(object): interfaces = {'required'} def set_required(self, value): if value in (True, 'true', 'True', '1'): self.xml.append(ET.Element("{%s}required" % self.namespace)) elif self['required']: self.del_required() def get_required(self): required = self.xml.find("{%s}required" % self.namespace) return required is not None def del_required(self): required = self.xml.find("{%s}required" % self.namespace) if required is not None: self.xml.remove(required) slixmpp-1.2.2/slixmpp/plugins/xep_0060/stanza/pubsub_errors.py0000644000175000001440000000575213004224717025371 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class PubsubErrorCondition(ElementBase): plugin_attrib = 'pubsub' interfaces = {'condition', 'unsupported'} plugin_attrib_map = {} plugin_tag_map = {} conditions = {'closed-node', 'configuration-required', 'invalid-jid', 'invalid-options', 'invalid-payload', 'invalid-subid', 'item-forbidden', 'item-required', 'jid-required', 'max-items-exceeded', 'max-nodes-exceeded', 'nodeid-required', 'not-in-roster-group', 'not-subscribed', 'payload-too-big', 'payload-required', 'pending-subscription', 'presence-subscription-required', 'subid-required', 'too-many-subscriptions', 'unsupported'} condition_ns = 'http://jabber.org/protocol/pubsub#errors' def setup(self, xml): """Don't create XML for the plugin.""" self.xml = ET.Element('') def get_condition(self): """Return the condition element's name.""" for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: return cond return '' def set_condition(self, value): """ Set the tag name of the condition element. Arguments: value -- The tag name of the condition element. """ if value in self.conditions: del self['condition'] cond = ET.Element("{%s}%s" % (self.condition_ns, value)) self.parent().xml.append(cond) return self def del_condition(self): """Remove the condition element.""" for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: self.parent().xml.remove(child) return self def get_unsupported(self): """Return the name of an unsupported feature""" xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns) if xml is not None: return xml.attrib.get('feature', '') return '' def set_unsupported(self, value): """Mark a feature as unsupported""" self.del_unsupported() xml = ET.Element('{%s}unsupported' % self.condition_ns) xml.attrib['feature'] = value self.parent().xml.append(xml) def del_unsupported(self): """Delete an unsupported feature condition.""" xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns) if xml is not None: self.parent().xml.remove(xml) register_stanza_plugin(Error, PubsubErrorCondition) slixmpp-1.2.2/slixmpp/plugins/xep_0323/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0323/sensordata.py0000644000175000001440000007267213004224717023347 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import time import datetime from threading import Thread, Lock, Timer from slixmpp.plugins.xep_0323.timerreset import TimerReset from slixmpp.xmlstream import JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0323 import stanza from slixmpp.plugins.xep_0323.stanza import Sensordata log = logging.getLogger(__name__) class XEP_0323(BasePlugin): """ XEP-0323: IoT Sensor Data This XEP provides the underlying architecture, basic operations and data structures for sensor data communication over XMPP networks. It includes a hardware abstraction model, removing any technical detail implemented in underlying technologies. Also see Configuration Values: threaded -- Indicates if communication with sensors should be threaded. Defaults to True. Events: Sensor side ----------- Sensordata Event:Req -- Received a request for data Sensordata Event:Cancel -- Received a cancellation for a request Client side ----------- Sensordata Event:Accepted -- Received a accept from sensor for a request Sensordata Event:Rejected -- Received a reject from sensor for a request Sensordata Event:Cancelled -- Received a cancel confirm from sensor Sensordata Event:Fields -- Received fields from sensor for a request This may be triggered multiple times since the sensor can split up its response in multiple messages. Sensordata Event:Failure -- Received a failure indication from sensor for a request. Typically a comm timeout. Attributes: threaded -- Indicates if command events should be threaded. Defaults to True. sessions -- A dictionary or equivalent backend mapping session IDs to dictionaries containing data relevant to a request's session. This dictionary is used both by the client and sensor side. On client side, seqnr is used as key, while on sensor side, a session_id is used as key. This ensures that the two will not collide, so one instance can be both client and sensor. Sensor side ----------- nodes -- A dictionary mapping sensor nodes that are serviced through this XMPP instance to their device handlers ("drivers"). Client side ----------- last_seqnr -- The last used sequence number (integer). One sequence of communication (e.g. -->request, <--accept, <--fields) between client and sensor is identified by a unique sequence number (unique between the client/sensor pair) Methods: plugin_init -- Overrides BasePlugin.plugin_init post_init -- Overrides BasePlugin.post_init plugin_end -- Overrides BasePlugin.plugin_end Sensor side ----------- register_node -- Register a sensor as available from this XMPP instance. Client side ----------- request_data -- Initiates a request for data from one or more sensors. Non-blocking, a callback function will be called when data is available. """ name = 'xep_0323' description = 'XEP-0323 Internet of Things - Sensor Data' dependencies = {'xep_0030'} stanza = stanza default_config = { 'threaded': True } def plugin_init(self): """ Start the XEP-0323 plugin """ self.xmpp.register_handler( Callback('Sensordata Event:Req', StanzaPath('iq@type=get/req'), self._handle_event_req)) self.xmpp.register_handler( Callback('Sensordata Event:Accepted', StanzaPath('iq@type=result/accepted'), self._handle_event_accepted)) self.xmpp.register_handler( Callback('Sensordata Event:Rejected', StanzaPath('iq@type=error/rejected'), self._handle_event_rejected)) self.xmpp.register_handler( Callback('Sensordata Event:Cancel', StanzaPath('iq@type=get/cancel'), self._handle_event_cancel)) self.xmpp.register_handler( Callback('Sensordata Event:Cancelled', StanzaPath('iq@type=result/cancelled'), self._handle_event_cancelled)) self.xmpp.register_handler( Callback('Sensordata Event:Fields', StanzaPath('message/fields'), self._handle_event_fields)) self.xmpp.register_handler( Callback('Sensordata Event:Failure', StanzaPath('message/failure'), self._handle_event_failure)) self.xmpp.register_handler( Callback('Sensordata Event:Started', StanzaPath('message/started'), self._handle_event_started)) # Server side dicts self.nodes = {} self.sessions = {} self.last_seqnr = 0 self.seqnr_lock = Lock() ## For testing only self.test_authenticated_from = "" def post_init(self): """ Init complete. Register our features in Service discovery. """ BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature(Sensordata.namespace) self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) def _new_session(self): """ Return a new session ID. """ return str(time.time()) + '-' + self.xmpp.new_id() def session_bind(self, jid): logging.debug("setting the Disco discovery for %s" % Sensordata.namespace) self.xmpp['xep_0030'].add_feature(Sensordata.namespace) self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) def plugin_end(self): """ Stop the XEP-0323 plugin """ self.sessions.clear() self.xmpp.remove_handler('Sensordata Event:Req') self.xmpp.remove_handler('Sensordata Event:Accepted') self.xmpp.remove_handler('Sensordata Event:Rejected') self.xmpp.remove_handler('Sensordata Event:Cancel') self.xmpp.remove_handler('Sensordata Event:Cancelled') self.xmpp.remove_handler('Sensordata Event:Fields') self.xmpp['xep_0030'].del_feature(feature=Sensordata.namespace) # ================================================================= # Sensor side (data provider) API def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): """ Register a sensor/device as available for serving of data through this XMPP instance. The device object may by any custom implementation to support specific devices, but it must implement the functions: has_field request_fields according to the interfaces shown in the example device.py file. Arguments: nodeId -- The identifier for the device device -- The device object commTimeout -- Time in seconds to wait between each callback from device during a data readout. Float. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ self.nodes[nodeId] = {"device": device, "commTimeout": commTimeout, "sourceId": sourceId, "cacheType": cacheType} def _set_authenticated(self, auth=''): """ Internal testing function """ self.test_authenticated_from = auth def _handle_event_req(self, iq): """ Event handler for reception of an Iq with req - this is a request. Verifies that - all the requested nodes are available - at least one of the requested fields is available from at least one of the nodes If the request passes verification, an accept response is sent, and the readout process is started in a separate thread. If the verification fails, a reject message is sent. """ seqnr = iq['req']['seqnr'] error_msg = '' req_ok = True # Authentication if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: # Invalid authentication req_ok = False error_msg = "Access denied" # Nodes process_nodes = [] if len(iq['req']['nodes']) > 0: for n in iq['req']['nodes']: if not n['nodeId'] in self.nodes: req_ok = False error_msg = "Invalid nodeId " + n['nodeId'] process_nodes = [n['nodeId'] for n in iq['req']['nodes']] else: process_nodes = self.nodes.keys() # Fields - if we just find one we are happy, otherwise we reject process_fields = [] if len(iq['req']['fields']) > 0: found = False for f in iq['req']['fields']: for node in self.nodes: if self.nodes[node]["device"].has_field(f['name']): found = True break if not found: req_ok = False error_msg = "Invalid field " + f['name'] process_fields = [f['name'] for n in iq['req']['fields']] req_flags = iq['req']._get_flags() request_delay_sec = None if 'when' in req_flags: # Timed request - requires datetime string in iso format # ex. 2013-04-05T15:00:03 dt = None try: dt = datetime.datetime.strptime(req_flags['when'], "%Y-%m-%dT%H:%M:%S") except ValueError: req_ok = False error_msg = "Invalid datetime in 'when' flag, please use ISO format (i.e. 2013-04-05T15:00:03)." if not dt is None: # Datetime properly formatted dtnow = datetime.datetime.now() dtdiff = dt - dtnow request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600 if request_delay_sec <= 0: req_ok = False error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat() if req_ok: session = self._new_session() self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr} self.sessions[session]["commTimers"] = {} self.sessions[session]["nodeDone"] = {} iq = iq.reply() iq['accepted']['seqnr'] = seqnr if not request_delay_sec is None: iq['accepted']['queued'] = "true" iq.send() self.sessions[session]["node_list"] = process_nodes if not request_delay_sec is None: # Delay request to requested time timer = Timer(request_delay_sec, self._event_delayed_req, args=(session, process_fields, req_flags)) self.sessions[session]["commTimers"]["delaytimer"] = timer timer.start() return if self.threaded: tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) tr_req.start() else: self._threaded_node_request(session, process_fields, req_flags) else: iq = iq.reply() iq['type'] = 'error' iq['rejected']['seqnr'] = seqnr iq['rejected']['error'] = error_msg iq.send() def _threaded_node_request(self, session, process_fields, flags): """ Helper function to handle the device readouts in a separate thread. Arguments: session -- The request session id process_fields -- The fields to request from the devices flags -- [optional] flags to pass to the devices, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } """ for node in self.sessions[session]["node_list"]: self.sessions[session]["nodeDone"][node] = False for node in self.sessions[session]["node_list"]: timer = TimerReset(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node)) self.sessions[session]["commTimers"][node] = timer timer.start() self.nodes[node]['device'].request_fields(process_fields, flags=flags, session=session, callback=self._device_field_request_callback) def _event_comm_timeout(self, session, nodeId): """ Triggered if any of the readout operations timeout. Sends a failure message back to the client, stops communicating with the failing device. Arguments: session -- The request session id nodeId -- The id of the device which timed out """ msg = self.xmpp.Message() msg['from'] = self.sessions[session]['to'] msg['to'] = self.sessions[session]['from'] msg['failure']['seqnr'] = self.sessions[session]['seqnr'] msg['failure']['error']['text'] = "Timeout" msg['failure']['error']['nodeId'] = nodeId msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() # Drop communication with this device and check if we are done self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): msg['failure']['done'] = 'true' msg.send() # The session is complete, delete it del self.sessions[session] def _event_delayed_req(self, session, process_fields, req_flags): """ Triggered when the timer from a delayed request fires. Arguments: session -- The request session id process_fields -- The fields to request from the devices flags -- [optional] flags to pass to the devices, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } """ msg = self.xmpp.Message() msg['from'] = self.sessions[session]['to'] msg['to'] = self.sessions[session]['from'] msg['started']['seqnr'] = self.sessions[session]['seqnr'] msg.send() if self.threaded: tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) tr_req.start() else: self._threaded_node_request(session, process_fields, req_flags) def _all_nodes_done(self, session): """ Checks whether all devices are done replying to the readout. Arguments: session -- The request session id """ for n in self.sessions[session]["nodeDone"]: if not self.sessions[session]["nodeDone"][n]: return False return True def _device_field_request_callback(self, session, nodeId, result, timestamp_block, error_msg=None): """ Callback function called by the devices when they have any additional data. Composes a message with the data and sends it back to the client, and resets the timeout timer for the device. Arguments: session -- The request session id nodeId -- The device id which initiated the callback result -- The current result status of the readout. Valid values are: "error" - Readout failed. "fields" - Contains readout data. "done" - Indicates that the readout is complete. May contain readout data. timestamp_block -- [optional] Only applies when result != "error" The readout data. Structured as a dictionary: { timestamp: timestamp for this datablock, fields: list of field dictionary (one per readout field). readout field dictionary format: { type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) name: The field name value: The field value unit: The unit of the field. Only applies to type numeric. dataType: The datatype of the field. Only applies to type enum. flags: [optional] data classifier flags for the field, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } } } error_msg -- [optional] Only applies when result == "error". Error details when a request failed. """ if not session in self.sessions: # This can happen if a session was deleted, like in a cancellation. Just drop the data. return if result == "error": self.sessions[session]["commTimers"][nodeId].cancel() msg = self.xmpp.Message() msg['from'] = self.sessions[session]['to'] msg['to'] = self.sessions[session]['from'] msg['failure']['seqnr'] = self.sessions[session]['seqnr'] msg['failure']['error']['text'] = error_msg msg['failure']['error']['nodeId'] = nodeId msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() # Drop communication with this device and check if we are done self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): msg['failure']['done'] = 'true' # The session is complete, delete it del self.sessions[session] msg.send() else: msg = self.xmpp.Message() msg['from'] = self.sessions[session]['to'] msg['to'] = self.sessions[session]['from'] msg['fields']['seqnr'] = self.sessions[session]['seqnr'] if timestamp_block is not None and len(timestamp_block) > 0: node = msg['fields'].add_node(nodeId) ts = node.add_timestamp(timestamp_block["timestamp"]) for f in timestamp_block["fields"]: data = ts.add_data( typename=f['type'], name=f['name'], value=f['value'], unit=f['unit'], dataType=f['dataType'], flags=f['flags']) if result == "done": self.sessions[session]["commTimers"][nodeId].cancel() self.sessions[session]["nodeDone"][nodeId] = True if (self._all_nodes_done(session)): # The session is complete, delete it del self.sessions[session] msg['fields']['done'] = 'true' else: # Restart comm timer self.sessions[session]["commTimers"][nodeId].reset() msg.send() def _handle_event_cancel(self, iq): """ Received Iq with cancel - this is a cancel request. Delete the session and confirm. """ seqnr = iq['cancel']['seqnr'] # Find the session for s in self.sessions: if self.sessions[s]['from'] == iq['from'] and self.sessions[s]['to'] == iq['to'] and self.sessions[s]['seqnr'] == seqnr: # found it. Cancel all timers for n in self.sessions[s]["commTimers"]: self.sessions[s]["commTimers"][n].cancel() # Confirm iq = iq.reply() iq['type'] = 'result' iq['cancelled']['seqnr'] = seqnr iq.send() # Delete session del self.sessions[s] return # Could not find session, send reject iq = iq.reply() iq['type'] = 'error' iq['rejected']['seqnr'] = seqnr iq['rejected']['error'] = "Cancel request received, no matching request is active." iq.send() # ================================================================= # Client side (data retriever) API def request_data(self, from_jid, to_jid, callback, nodeIds=None, fields=None, flags=None): """ Called on the client side to initiate a data readout. Composes a message with the request and sends it to the device(s). Does not block, the callback will be called when data is available. Arguments: from_jid -- The jid of the requester to_jid -- The jid of the device(s) callback -- The callback function to call when data is available. The callback function must support the following arguments: from_jid -- The jid of the responding device(s) result -- The current result status of the readout. Valid values are: "accepted" - Readout request accepted "queued" - Readout request accepted and queued "rejected" - Readout request rejected "failure" - Readout failed. "cancelled" - Confirmation of request cancellation. "started" - Previously queued request is now started "fields" - Contains readout data. "done" - Indicates that the readout is complete. nodeId -- [optional] Mandatory when result == "fields" or "failure". The node Id of the responding device. One callback will only contain data from one device. timestamp -- [optional] Mandatory when result == "fields". The timestamp of data in this callback. One callback will only contain data from one timestamp. fields -- [optional] Mandatory when result == "fields". List of field dictionaries representing the readout data. Dictionary format: { typename: The field type (numeric, boolean, dateTime, timeSpan, string, enum) name: The field name value: The field value unit: The unit of the field. Only applies to type numeric. dataType: The datatype of the field. Only applies to type enum. flags: [optional] data classifier flags for the field, e.g. momentary. Formatted as a dictionary like { "flag name": "flag value" ... } } error_msg -- [optional] Mandatory when result == "rejected" or "failure". Details about why the request is rejected or failed. "rejected" means that the request is stopped, but note that the request will continue even after a "failure". "failure" only means that communication was stopped to that specific device, other device(s) (if any) will continue their readout. nodeIds -- [optional] Limits the request to the node Ids in this list. fields -- [optional] Limits the request to the field names in this list. flags -- [optional] Limits the request according to the flags, or sets readout conditions such as timing. Return value: session -- Session identifier. Client can use this as a reference to cancel the request. """ iq = self.xmpp.Iq() iq['from'] = from_jid iq['to'] = to_jid iq['type'] = "get" seqnr = self._get_new_seqnr() iq['id'] = seqnr iq['req']['seqnr'] = seqnr if nodeIds is not None: for nodeId in nodeIds: iq['req'].add_node(nodeId) if fields is not None: for field in fields: iq['req'].add_field(field) iq['req']._set_flags(flags) self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr, "callback": callback} iq.send() return seqnr def cancel_request(self, session): """ Called on the client side to cancel a request for data readout. Composes a message with the cancellation and sends it to the device(s). Does not block, the callback will be called when cancellation is confirmed. Arguments: session -- The session id of the request to cancel """ seqnr = session iq = self.xmpp.Iq() iq['from'] = self.sessions[seqnr]['from'] iq['to'] = self.sessions[seqnr]['to'] iq['type'] = "get" iq['id'] = seqnr iq['cancel']['seqnr'] = seqnr iq.send() def _get_new_seqnr(self): """ Returns a unique sequence number (unique across threads) """ self.seqnr_lock.acquire() self.last_seqnr += 1 self.seqnr_lock.release() return str(self.last_seqnr) def _handle_event_accepted(self, iq): """ Received Iq with accepted - request was accepted """ seqnr = iq['accepted']['seqnr'] result = "accepted" if iq['accepted']['queued'] == 'true': result = "queued" callback = self.sessions[seqnr]["callback"] callback(from_jid=iq['from'], result=result) def _handle_event_rejected(self, iq): """ Received Iq with rejected - this is a reject. Delete the session. """ seqnr = iq['rejected']['seqnr'] callback = self.sessions[seqnr]["callback"] callback(from_jid=iq['from'], result="rejected", error_msg=iq['rejected']['error']) # Session terminated del self.sessions[seqnr] def _handle_event_cancelled(self, iq): """ Received Iq with cancelled - this is a cancel confirm. Delete the session. """ seqnr = iq['cancelled']['seqnr'] callback = self.sessions[seqnr]["callback"] callback(from_jid=iq['from'], result="cancelled") # Session cancelled del self.sessions[seqnr] def _handle_event_fields(self, msg): """ Received Msg with fields - this is a data response to a request. If this is the last data block, issue a "done" callback. """ seqnr = msg['fields']['seqnr'] callback = self.sessions[seqnr]["callback"] for node in msg['fields']['nodes']: for ts in node['timestamps']: fields = [] for d in ts['datas']: field_block = {} field_block["name"] = d['name'] field_block["typename"] = d._get_typename() field_block["value"] = d['value'] if not d['unit'] == "": field_block["unit"] = d['unit'] if not d['dataType'] == "": field_block["dataType"] = d['dataType'] flags = d._get_flags() if not len(flags) == 0: field_block["flags"] = flags fields.append(field_block) callback(from_jid=msg['from'], result="fields", nodeId=node['nodeId'], timestamp=ts['value'], fields=fields) if msg['fields']['done'] == "true": callback(from_jid=msg['from'], result="done") # Session done del self.sessions[seqnr] def _handle_event_failure(self, msg): """ Received Msg with failure - our request failed Delete the session. """ seqnr = msg['failure']['seqnr'] callback = self.sessions[seqnr]["callback"] callback(from_jid=msg['from'], result="failure", nodeId=msg['failure']['error']['nodeId'], timestamp=msg['failure']['error']['timestamp'], error_msg=msg['failure']['error']['text']) # Session failed del self.sessions[seqnr] def _handle_event_started(self, msg): """ Received Msg with started - our request was queued and is now started. """ seqnr = msg['started']['seqnr'] callback = self.sessions[seqnr]["callback"] callback(from_jid=msg['from'], result="started") slixmpp-1.2.2/slixmpp/plugins/xep_0323/device.py0000644000175000001440000002345112603534742022440 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime import logging class Device(object): """ Example implementation of a device readout object. Is registered in the XEP_0323.register_node call The device object may be any custom implementation to support specific devices, but it must implement the functions: has_field request_fields """ def __init__(self, nodeId, fields=None): if not fields: fields = {} self.nodeId = nodeId self.fields = fields # see fields described below # {'type':'numeric', # 'name':'myname', # 'value': 42, # 'unit':'Z'}] self.timestamp_data = {} self.momentary_data = {} self.momentary_timestamp = "" logging.debug("Device object started nodeId %s",nodeId) def has_field(self, field): """ Returns true if the supplied field name exists in this device. Arguments: field -- The field name """ if field in self.fields.keys(): return True return False def refresh(self, fields): """ override method to do the refresh work refresh values from hardware or other """ pass def request_fields(self, fields, flags, session, callback): """ Starts a data readout. Verifies the requested fields, refreshes the data (if needed) and calls the callback with requested data. Arguments: fields -- List of field names to readout flags -- [optional] data classifier flags for the field, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } session -- Session id, only used in the callback as identifier callback -- Callback function to call when data is available. The callback function must support the following arguments: session -- Session id, as supplied in the request_fields call nodeId -- Identifier for this device result -- The current result status of the readout. Valid values are: "error" - Readout failed. "fields" - Contains readout data. "done" - Indicates that the readout is complete. May contain readout data. timestamp_block -- [optional] Only applies when result != "error" The readout data. Structured as a dictionary: { timestamp: timestamp for this datablock, fields: list of field dictionary (one per readout field). readout field dictionary format: { type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) name: The field name value: The field value unit: The unit of the field. Only applies to type numeric. dataType: The datatype of the field. Only applies to type enum. flags: [optional] data classifier flags for the field, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } } } error_msg -- [optional] Only applies when result == "error". Error details when a request failed. """ logging.debug("request_fields called looking for fields %s",fields) if len(fields) > 0: # Check availiability for f in fields: if f not in self.fields.keys(): self._send_reject(session, callback) return False else: # Request all fields fields = self.fields.keys() # Refresh data from device # ... logging.debug("about to refresh device fields %s",fields) self.refresh(fields) if "momentary" in flags and flags['momentary'] == "true" or \ "all" in flags and flags['all'] == "true": ts_block = {} timestamp = "" if len(self.momentary_timestamp) > 0: timestamp = self.momentary_timestamp else: timestamp = self._get_timestamp() field_block = [] for f in self.momentary_data: if f in fields: field_block.append({"name": f, "type": self.fields[f]["type"], "unit": self.fields[f]["unit"], "dataType": self.fields[f]["dataType"], "value": self.momentary_data[f]["value"], "flags": self.momentary_data[f]["flags"]}) ts_block["timestamp"] = timestamp ts_block["fields"] = field_block callback(session, result="done", nodeId=self.nodeId, timestamp_block=ts_block) return from_flag = self._datetime_flag_parser(flags, 'from') to_flag = self._datetime_flag_parser(flags, 'to') for ts in sorted(self.timestamp_data.keys()): tsdt = datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S") if not from_flag is None: if tsdt < from_flag: #print (str(tsdt) + " < " + str(from_flag)) continue if not to_flag is None: if tsdt > to_flag: #print (str(tsdt) + " > " + str(to_flag)) continue ts_block = {} field_block = [] for f in self.timestamp_data[ts]: if f in fields: field_block.append({"name": f, "type": self.fields[f]["type"], "unit": self.fields[f]["unit"], "dataType": self.fields[f]["dataType"], "value": self.timestamp_data[ts][f]["value"], "flags": self.timestamp_data[ts][f]["flags"]}) ts_block["timestamp"] = ts ts_block["fields"] = field_block callback(session, result="fields", nodeId=self.nodeId, timestamp_block=ts_block) callback(session, result="done", nodeId=self.nodeId, timestamp_block=None) def _datetime_flag_parser(self, flags, flagname): if not flagname in flags: return None dt = None try: dt = datetime.datetime.strptime(flags[flagname], "%Y-%m-%dT%H:%M:%S") except ValueError: # Badly formatted datetime, ignore it pass return dt def _get_timestamp(self): """ Generates a properly formatted timestamp of current time """ return datetime.datetime.now().replace(microsecond=0).isoformat() def _send_reject(self, session, callback): """ Sends a reject to the caller Arguments: session -- Session id, see definition in request_fields function callback -- Callback function, see definition in request_fields function """ callback(session, result="error", nodeId=self.nodeId, timestamp_block=None, error_msg="Reject") def _add_field(self, name, typename, unit=None, dataType=None): """ Adds a field to the device Arguments: name -- Name of the field typename -- Type of the field (numeric, boolean, dateTime, timeSpan, string, enum) unit -- [optional] only applies to "numeric". Unit for the field. dataType -- [optional] only applies to "enum". Datatype for the field. """ self.fields[name] = {"type": typename, "unit": unit, "dataType": dataType} def _add_field_timestamp_data(self, name, timestamp, value, flags=None): """ Adds timestamped data to a field Arguments: name -- Name of the field timestamp -- Timestamp for the data (string) value -- Field value at the timestamp flags -- [optional] data classifier flags for the field, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } """ if not name in self.fields.keys(): return False if not timestamp in self.timestamp_data: self.timestamp_data[timestamp] = {} self.timestamp_data[timestamp][name] = {"value": value, "flags": flags} return True def _add_field_momentary_data(self, name, value, flags=None): """ Sets momentary data to a field Arguments: name -- Name of the field value -- Field value at the timestamp flags -- [optional] data classifier flags for the field, e.g. momentary Formatted as a dictionary like { "flag name": "flag value" ... } """ if name not in self.fields: return False if flags is None: flags = {} flags["momentary"] = "true" self.momentary_data[name] = {"value": value, "flags": flags} return True def _set_momentary_timestamp(self, timestamp): """ This function is only for unit testing to produce predictable results. """ self.momentary_timestamp = timestamp slixmpp-1.2.2/slixmpp/plugins/xep_0323/__init__.py0000644000175000001440000000101712424504520022722 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0323.sensordata import XEP_0323 from slixmpp.plugins.xep_0323 import stanza register_plugin(XEP_0323) xep_0323=XEP_0323 slixmpp-1.2.2/slixmpp/plugins/xep_0323/timerreset.py0000644000175000001440000000403312603534742023357 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from threading import Thread, Event, Timer import time def TimerReset(*args, **kwargs): """ Global function for Timer """ return _TimerReset(*args, **kwargs) class _TimerReset(Thread): """Call a function after a specified number of seconds: t = TimerReset(30.0, f, args=[], kwargs={}) t.start() t.cancel() # stop the timer's action if it's still waiting """ def __init__(self, interval, function, args=None, kwargs=None): if not kwargs: kwargs = {} if not args: args = [] Thread.__init__(self) self.interval = interval self.function = function self.args = args self.kwargs = kwargs self.finished = Event() self.resetted = True def cancel(self): """Stop the timer if it hasn't finished yet""" self.finished.set() def run(self): #print "Time: %s - timer running..." % time.asctime() while self.resetted: #print "Time: %s - timer waiting for timeout in %.2f..." % (time.asctime(), self.interval) self.resetted = False self.finished.wait(self.interval) if not self.finished.isSet(): self.function(*self.args, **self.kwargs) self.finished.set() #print "Time: %s - timer finished!" % time.asctime() def reset(self, interval=None): """ Reset the timer """ if interval: #print "Time: %s - timer resetting to %.2f..." % (time.asctime(), interval) self.interval = interval else: #print "Time: %s - timer resetting..." % time.asctime() pass self.resetted = True self.finished.set() self.finished.clear() slixmpp-1.2.2/slixmpp/plugins/xep_0323/stanza/0000755000175000001440000000000013014656513022120 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0323/stanza/sensordata.py0000644000175000001440000006153713010437511024640 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Iq, Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID import re class Sensordata(ElementBase): """ Placeholder for the namespace, not used as a stanza """ namespace = 'urn:xmpp:iot:sensordata' name = 'sensordata' plugin_attrib = name interfaces = set() class FieldTypes(): """ All field types are optional booleans that default to False """ field_types = {'momentary', 'peak', 'status', 'computed', 'identity', 'historicalSecond', 'historicalMinute', 'historicalHour', 'historicalDay', 'historicalWeek', 'historicalMonth', 'historicalQuarter', 'historicalYear', 'historicalOther'} class FieldStatus(): """ All field statuses are optional booleans that default to False """ field_status = {'missing', 'automaticEstimate', 'manualEstimate', 'manualReadout', 'automaticReadout', 'timeOffset', 'warning', 'error', 'signed', 'invoiced', 'endOfSeries', 'powerFailure', 'invoiceConfirmed'} class Request(ElementBase): namespace = 'urn:xmpp:iot:sensordata' name = 'req' plugin_attrib = name interfaces = {'seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all'} interfaces.update(FieldTypes.field_types) _flags = {'serviceToken','deviceToken','userToken','from','to','when','historical','all'} _flags.update(FieldTypes.field_types) def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._nodes = set() self._fields = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._nodes = {node['nodeId'] for node in self['nodes']} self._fields = {field['name'] for field in self['fields']} def _get_flags(self): """ Helper function for getting of flags. Returns all flags in dictionary format: { "flag name": "flag value" ... } """ flags = {} for f in self._flags: if not self[f] == "": flags[f] = self[f] return flags def _set_flags(self, flags): """ Helper function for setting of flags. Arguments: flags -- Flags in dictionary format: { "flag name": "flag value" ... } """ for f in self._flags: if flags is not None and f in flags: self[f] = flags[f] else: self[f] = None def add_node(self, nodeId, sourceId=None, cacheType=None): """ Add a new node element. Each item is required to have a nodeId, but may also specify a sourceId value and cacheType. Arguments: nodeId -- The ID for the node. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ if nodeId not in self._nodes: self._nodes.add((nodeId)) node = RequestNode(parent=self) node['nodeId'] = nodeId node['sourceId'] = sourceId node['cacheType'] = cacheType self.iterables.append(node) return node return None def del_node(self, nodeId): """ Remove a single node. Arguments: nodeId -- Node ID of the item to remove. """ if nodeId in self._nodes: nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: if node['nodeId'] == nodeId: self.xml.remove(node.xml) self.iterables.remove(node) return True return False def get_nodes(self): """Return all nodes.""" nodes = [] for node in self['substanzas']: if isinstance(node, RequestNode): nodes.append(node) return nodes def set_nodes(self, nodes): """ Set or replace all nodes. The given nodes must be in a list or set where each item is a tuple of the form: (nodeId, sourceId, cacheType) Arguments: nodes -- A series of nodes in tuple format. """ self.del_nodes() for node in nodes: if isinstance(node, RequestNode): self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) else: nodeId, sourceId, cacheType = node self.add_node(nodeId, sourceId, cacheType) def del_nodes(self): """Remove all nodes.""" self._nodes = set() nodes = [i for i in self.iterables if isinstance(i, RequestNode)] for node in nodes: self.xml.remove(node.xml) self.iterables.remove(node) def add_field(self, name): """ Add a new field element. Each item is required to have a name. Arguments: name -- The name of the field. """ if name not in self._fields: self._fields.add((name)) field = RequestField(parent=self) field['name'] = name self.iterables.append(field) return field return None def del_field(self, name): """ Remove a single field. Arguments: name -- name of field to remove. """ if name in self._fields: fields = [i for i in self.iterables if isinstance(i, RequestField)] for field in fields: if field['name'] == name: self.xml.remove(field.xml) self.iterables.remove(field) return True return False def get_fields(self): """Return all fields.""" fields = [] for field in self['substanzas']: if isinstance(field, RequestField): fields.append(field) return fields def set_fields(self, fields): """ Set or replace all fields. The given fields must be in a list or set where each item is RequestField or string Arguments: fields -- A series of fields in RequestField or string format. """ self.del_fields() for field in fields: if isinstance(field, RequestField): self.add_field(field['name']) else: self.add_field(field) def del_fields(self): """Remove all fields.""" self._fields = set() fields = [i for i in self.iterables if isinstance(i, RequestField)] for field in fields: self.xml.remove(field.xml) self.iterables.remove(field) class RequestNode(ElementBase): """ Node element in a request """ namespace = 'urn:xmpp:iot:sensordata' name = 'node' plugin_attrib = name interfaces = {'nodeId','sourceId','cacheType'} class RequestField(ElementBase): """ Field element in a request """ namespace = 'urn:xmpp:iot:sensordata' name = 'field' plugin_attrib = name interfaces = {'name'} class Accepted(ElementBase): namespace = 'urn:xmpp:iot:sensordata' name = 'accepted' plugin_attrib = name interfaces = {'seqnr','queued'} class Started(ElementBase): namespace = 'urn:xmpp:iot:sensordata' name = 'started' plugin_attrib = name interfaces = {'seqnr'} class Failure(ElementBase): namespace = 'urn:xmpp:iot:sensordata' name = 'failure' plugin_attrib = name interfaces = {'seqnr','done'} class Error(ElementBase): """ Error element in a request failure """ namespace = 'urn:xmpp:iot:sensordata' name = 'error' plugin_attrib = name interfaces = {'nodeId','timestamp','sourceId','cacheType','text'} def get_text(self): """Return then contents inside the XML tag.""" return self.xml.text def set_text(self, value): """Set then contents inside the XML tag. :param value: string """ self.xml.text = value return self def del_text(self): """Remove the contents inside the XML tag.""" self.xml.text = "" return self class Rejected(ElementBase): namespace = 'urn:xmpp:iot:sensordata' name = 'rejected' plugin_attrib = name interfaces = {'seqnr','error'} sub_interfaces = {'error'} class Fields(ElementBase): """ Fields element, top level in a response message with data """ namespace = 'urn:xmpp:iot:sensordata' name = 'fields' plugin_attrib = name interfaces = {'seqnr','done','nodes'} def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._nodes = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._nodes = {node['nodeId'] for node in self['nodes']} def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None): """ Add a new node element. Each item is required to have a nodeId, but may also specify a sourceId value and cacheType. Arguments: nodeId -- The ID for the node. sourceId -- [optional] identifying the data source controlling the device cacheType -- [optional] narrowing down the search to a specific kind of node """ if nodeId not in self._nodes: self._nodes.add((nodeId)) node = FieldsNode(parent=self) node['nodeId'] = nodeId node['sourceId'] = sourceId node['cacheType'] = cacheType if substanzas is not None: node.set_timestamps(substanzas) self.iterables.append(node) return node return None def del_node(self, nodeId): """ Remove a single node. Arguments: nodeId -- Node ID of the item to remove. """ if nodeId in self._nodes: nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] for node in nodes: if node['nodeId'] == nodeId: self.xml.remove(node.xml) self.iterables.remove(node) return True return False def get_nodes(self): """Return all nodes.""" nodes = [] for node in self['substanzas']: if isinstance(node, FieldsNode): nodes.append(node) return nodes def set_nodes(self, nodes): """ Set or replace all nodes. The given nodes must be in a list or set where each item is a tuple of the form: (nodeId, sourceId, cacheType) Arguments: nodes -- A series of nodes in tuple format. """ #print(str(id(self)) + " set_nodes: got " + str(nodes)) self.del_nodes() for node in nodes: if isinstance(node, FieldsNode): self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas']) else: nodeId, sourceId, cacheType = node self.add_node(nodeId, sourceId, cacheType) def del_nodes(self): """Remove all nodes.""" self._nodes = set() nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] for node in nodes: self.xml.remove(node.xml) self.iterables.remove(node) class FieldsNode(ElementBase): """ Node element in response fields """ namespace = 'urn:xmpp:iot:sensordata' name = 'node' plugin_attrib = name interfaces = {'nodeId','sourceId','cacheType','timestamps'} def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._timestamps = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._timestamps = {ts['value'] for ts in self['timestamps']} def add_timestamp(self, timestamp, substanzas=None): """ Add a new timestamp element. Arguments: timestamp -- The timestamp in ISO format. """ #print(str(id(self)) + " add_timestamp: " + str(timestamp)) if timestamp not in self._timestamps: self._timestamps.add((timestamp)) ts = Timestamp(parent=self) ts['value'] = timestamp if not substanzas is None: ts.set_datas(substanzas) #print("add_timestamp with substanzas: " + str(substanzas)) self.iterables.append(ts) #print(str(id(self)) + " added_timestamp: " + str(id(ts))) return ts return None def del_timestamp(self, timestamp): """ Remove a single timestamp. Arguments: timestamp -- timestamp (in ISO format) of the item to remove. """ #print("del_timestamp: ") if timestamp in self._timestamps: timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] for ts in timestamps: if ts['value'] == timestamp: self.xml.remove(ts.xml) self.iterables.remove(ts) return True return False def get_timestamps(self): """Return all timestamps.""" #print(str(id(self)) + " get_timestamps: ") timestamps = [] for timestamp in self['substanzas']: if isinstance(timestamp, Timestamp): timestamps.append(timestamp) return timestamps def set_timestamps(self, timestamps): """ Set or replace all timestamps. The given timestamps must be in a list or set where each item is a timestamp Arguments: timestamps -- A series of timestamps. """ #print(str(id(self)) + " set_timestamps: got " + str(timestamps)) self.del_timestamps() for timestamp in timestamps: #print("set_timestamps: subset " + str(timestamp)) #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas'])) if isinstance(timestamp, Timestamp): self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas']) else: #print("set_timestamps: got " + str(timestamp)) self.add_timestamp(timestamp) def del_timestamps(self): """Remove all timestamps.""" #print(str(id(self)) + " del_timestamps: ") self._timestamps = set() timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] for timestamp in timestamps: self.xml.remove(timestamp.xml) self.iterables.remove(timestamp) class Field(ElementBase): """ Field element in response Timestamp. This is a base class, all instances of fields added to Timestamp must be of types: DataNumeric DataString DataBoolean DataDateTime DataTimeSpan DataEnum """ namespace = 'urn:xmpp:iot:sensordata' name = 'field' plugin_attrib = name interfaces = {'name','module','stringIds'} interfaces.update(FieldTypes.field_types) interfaces.update(FieldStatus.field_status) _flags = set() _flags.update(FieldTypes.field_types) _flags.update(FieldStatus.field_status) def set_stringIds(self, value): """Verifies stringIds according to regexp from specification XMPP-0323. :param value: string """ pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$") if pattern.match(value) is not None: self.xml.stringIds = value else: # Bad content, add nothing pass return self def _get_flags(self): """ Helper function for getting of flags. Returns all flags in dictionary format: { "flag name": "flag value" ... } """ flags = {} for f in self._flags: if not self[f] == "": flags[f] = self[f] return flags def _set_flags(self, flags): """ Helper function for setting of flags. Arguments: flags -- Flags in dictionary format: { "flag name": "flag value" ... } """ for f in self._flags: if flags is not None and f in flags: self[f] = flags[f] else: self[f] = None def _get_typename(self): return "invalid type, use subclasses!" class Timestamp(ElementBase): """ Timestamp element in response Node """ namespace = 'urn:xmpp:iot:sensordata' name = 'timestamp' plugin_attrib = name interfaces = {'value','datas'} def __init__(self, xml=None, parent=None): ElementBase.__init__(self, xml, parent) self._datas = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._datas = {data['name'] for data in self['datas']} def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None): """ Add a new data element. Arguments: typename -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum) value -- The value of the data element module -- [optional] language module to use for the data element stringIds -- [optional] The stringIds used to find associated text in the language module unit -- [optional] The unit. Only applicable for type numeric dataType -- [optional] The dataType. Only applicable for type enum """ if name not in self._datas: dataObj = None if typename == "numeric": dataObj = DataNumeric(parent=self) dataObj['unit'] = unit elif typename == "string": dataObj = DataString(parent=self) elif typename == "boolean": dataObj = DataBoolean(parent=self) elif typename == "dateTime": dataObj = DataDateTime(parent=self) elif typename == "timeSpan": dataObj = DataTimeSpan(parent=self) elif typename == "enum": dataObj = DataEnum(parent=self) dataObj['dataType'] = dataType dataObj['name'] = name dataObj['value'] = value dataObj['module'] = module dataObj['stringIds'] = stringIds if flags is not None: dataObj._set_flags(flags) self._datas.add(name) self.iterables.append(dataObj) return dataObj return None def del_data(self, name): """ Remove a single data element. Arguments: data_name -- The data element name to remove. """ if name in self._datas: datas = [i for i in self.iterables if isinstance(i, Field)] for data in datas: if data['name'] == name: self.xml.remove(data.xml) self.iterables.remove(data) return True return False def get_datas(self): """ Return all data elements. """ datas = [] for data in self['substanzas']: if isinstance(data, Field): datas.append(data) return datas def set_datas(self, datas): """ Set or replace all data elements. The given elements must be in a list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) Arguments: datas -- A series of data elements. """ self.del_datas() for data in datas: self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags()) def del_datas(self): """Remove all data elements.""" self._datas = set() datas = [i for i in self.iterables if isinstance(i, Field)] for data in datas: self.xml.remove(data.xml) self.iterables.remove(data) class DataNumeric(Field): """ Field data of type numeric. Note that the value is expressed as a string. """ namespace = 'urn:xmpp:iot:sensordata' name = 'numeric' plugin_attrib = name interfaces = {'value', 'unit'} interfaces.update(Field.interfaces) def _get_typename(self): return "numeric" class DataString(Field): """ Field data of type string """ namespace = 'urn:xmpp:iot:sensordata' name = 'string' plugin_attrib = name interfaces = {'value'} interfaces.update(Field.interfaces) def _get_typename(self): return "string" class DataBoolean(Field): """ Field data of type boolean. Note that the value is expressed as a string. """ namespace = 'urn:xmpp:iot:sensordata' name = 'boolean' plugin_attrib = name interfaces = {'value'} interfaces.update(Field.interfaces) def _get_typename(self): return "boolean" class DataDateTime(Field): """ Field data of type dateTime. Note that the value is expressed as a string. """ namespace = 'urn:xmpp:iot:sensordata' name = 'dateTime' plugin_attrib = name interfaces = {'value'} interfaces.update(Field.interfaces) def _get_typename(self): return "dateTime" class DataTimeSpan(Field): """ Field data of type timeSpan. Note that the value is expressed as a string. """ namespace = 'urn:xmpp:iot:sensordata' name = 'timeSpan' plugin_attrib = name interfaces = {'value'} interfaces.update(Field.interfaces) def _get_typename(self): return "timeSpan" class DataEnum(Field): """ Field data of type enum. Note that the value is expressed as a string. """ namespace = 'urn:xmpp:iot:sensordata' name = 'enum' plugin_attrib = name interfaces = {'value', 'dataType'} interfaces.update(Field.interfaces) def _get_typename(self): return "enum" class Done(ElementBase): """ Done element used to signal that all data has been transferred """ namespace = 'urn:xmpp:iot:sensordata' name = 'done' plugin_attrib = name interfaces = {'seqnr'} class Cancel(ElementBase): """ Cancel element used to signal that a request shall be cancelled """ namespace = 'urn:xmpp:iot:sensordata' name = 'cancel' plugin_attrib = name interfaces = {'seqnr'} class Cancelled(ElementBase): """ Cancelled element used to signal that cancellation is confirmed """ namespace = 'urn:xmpp:iot:sensordata' name = 'cancelled' plugin_attrib = name interfaces = {'seqnr'} register_stanza_plugin(Iq, Request) register_stanza_plugin(Request, RequestNode, iterable=True) register_stanza_plugin(Request, RequestField, iterable=True) register_stanza_plugin(Iq, Accepted) register_stanza_plugin(Message, Failure) register_stanza_plugin(Failure, Error) register_stanza_plugin(Iq, Rejected) register_stanza_plugin(Message, Fields) register_stanza_plugin(Fields, FieldsNode, iterable=True) register_stanza_plugin(FieldsNode, Timestamp, iterable=True) register_stanza_plugin(Timestamp, Field, iterable=True) register_stanza_plugin(Timestamp, DataNumeric, iterable=True) register_stanza_plugin(Timestamp, DataString, iterable=True) register_stanza_plugin(Timestamp, DataBoolean, iterable=True) register_stanza_plugin(Timestamp, DataDateTime, iterable=True) register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True) register_stanza_plugin(Timestamp, DataEnum, iterable=True) register_stanza_plugin(Message, Started) register_stanza_plugin(Iq, Cancel) register_stanza_plugin(Iq, Cancelled) slixmpp-1.2.2/slixmpp/plugins/xep_0323/stanza/__init__.py0000644000175000001440000000060412424504520024223 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0323.stanza.sensordata import * slixmpp-1.2.2/slixmpp/plugins/xep_0323/stanza/base.py0000644000175000001440000000056112424504520023400 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Implementation of xeps for Internet of Things http://wiki.xmpp.org/web/Tech_pages/IoT_systems Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ET pass slixmpp-1.2.2/slixmpp/plugins/xep_0352/0000755000175000001440000000000013014656513020622 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0352/__init__.py0000644000175000001440000000062112724635251022735 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0352.stanza import Active, Inactive, ClientStateIndication from slixmpp.plugins.xep_0352.csi import XEP_0352 register_plugin(XEP_0352) slixmpp-1.2.2/slixmpp/plugins/xep_0352/csi.py0000644000175000001440000000370112724640425021755 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0352 import stanza, Active, Inactive, ClientStateIndication log = logging.getLogger(__name__) class XEP_0352(BasePlugin): """ XEP-0352: Client State Indication """ name = 'xep_0352' description = 'XEP-0352: Client State Indication' dependencies = set() stanza = stanza default_config = { "order": 12000, } def plugin_init(self): """Start the XEP-0352 plugin.""" self.enabled = False register_stanza_plugin(StreamFeatures, ClientStateIndication) self.xmpp.register_stanza(stanza.Active) self.xmpp.register_stanza(stanza.Inactive) self.xmpp.register_feature('csi', self._handle_csi_feature, restart=False, order=self.order) def plugin_end(self): if self.xmpp.is_component: return self.xmpp.unregister_feature('csi', self.order) self.xmpp.remove_stanza(stanza.Active) self.xmpp.remove_stanza(stanza.Inactive) def send_active(self): """Send an 'active' state""" if self.enabled: self.xmpp.send_raw(str(stanza.Active(self.xmpp))) def send_inactive(self): """Send an 'active' state""" if self.enabled: self.xmpp.send_raw(str(stanza.Inactive(self.xmpp))) def _handle_csi_feature(self, features): """ Enable CSI """ if 'csi' in self.xmpp.features: log.debug('CSI already enabled') return False self.enabled = True self.xmpp.event('csi_enabled', features) return False slixmpp-1.2.2/slixmpp/plugins/xep_0352/stanza.py0000644000175000001440000000146712724640065022506 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, StanzaBase class ClientStateIndication(ElementBase): name = 'csi' namespace = 'urn:xmpp:csi:0' plugin_attrib = name class Active(StanzaBase): name = 'active' plugin_attrib = 'active' namespace = 'urn:xmpp:csi:0' def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class Inactive (StanzaBase): name = 'inactive' plugin_attrib = 'inactive' namespace = 'urn:xmpp:csi:0' def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() slixmpp-1.2.2/slixmpp/plugins/xep_0297/0000755000175000001440000000000013014656513020632 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0297/forwarded.py0000644000175000001440000000403213004224717023154 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq, Message, Presence from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0297 import stanza, Forwarded class XEP_0297(BasePlugin): name = 'xep_0297' description = 'XEP-0297: Stanza Forwarding' dependencies = {'xep_0030', 'xep_0203'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, Forwarded) # While these are marked as iterable, that is just for # making it easier to extract the forwarded stanza. There # still can be only a single forwarded stanza. register_stanza_plugin(Forwarded, Message, iterable=True) register_stanza_plugin(Forwarded, Presence, iterable=True) register_stanza_plugin(Forwarded, Iq, iterable=True) register_stanza_plugin(Forwarded, self.xmpp['xep_0203'].stanza.Delay) self.xmpp.register_handler( Callback('Forwarded Stanza', StanzaPath('message/forwarded'), self._handle_forwarded)) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:forward:0') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:forward:0') self.xmpp.remove_handler('Forwarded Stanza') def forward(self, stanza=None, mto=None, mbody=None, mfrom=None, delay=None): stanza.stream = None msg = self.xmpp.Message() msg['to'] = mto msg['from'] = mfrom msg['body'] = mbody msg['forwarded']['stanza'] = stanza if delay is not None: msg['forwarded']['delay']['stamp'] = delay msg.send() def _handle_forwarded(self, msg): self.xmpp.event('forwarded_stanza', msg) slixmpp-1.2.2/slixmpp/plugins/xep_0297/__init__.py0000644000175000001440000000064512424504520022742 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0297 import stanza from slixmpp.plugins.xep_0297.stanza import Forwarded from slixmpp.plugins.xep_0297.forwarded import XEP_0297 register_plugin(XEP_0297) slixmpp-1.2.2/slixmpp/plugins/xep_0297/stanza.py0000644000175000001440000000173413004224717022505 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Message, Presence, Iq from slixmpp.xmlstream import ElementBase class Forwarded(ElementBase): name = 'forwarded' namespace = 'urn:xmpp:forward:0' plugin_attrib = 'forwarded' interfaces = {'stanza'} def get_stanza(self): for stanza in self: if isinstance(stanza, (Message, Presence, Iq)): return stanza return '' def set_stanza(self, value): self.del_stanza() self.append(value) def del_stanza(self): found_stanzas = [] for stanza in self: if isinstance(stanza, (Message, Presence, Iq)): found_stanzas.append(stanza) for stanza in found_stanzas: self.iterables.remove(stanza) self.xml.remove(stanza.xml) slixmpp-1.2.2/slixmpp/plugins/xep_0004/0000755000175000001440000000000013014656513020614 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0004/dataforms.py0000644000175000001440000000315713004224717023150 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import stanza from slixmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption class XEP_0004(BasePlugin): """ XEP-0004: Data Forms """ name = 'xep_0004' description = 'XEP-0004: Data Forms' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Data Form', StanzaPath('message/form'), self.handle_form)) register_stanza_plugin(FormField, FieldOption, iterable=True) register_stanza_plugin(Form, FormField, iterable=True) register_stanza_plugin(Message, Form) def plugin_end(self): self.xmpp.remove_handler('Data Form') self.xmpp['xep_0030'].del_feature(feature='jabber:x:data') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('jabber:x:data') def make_form(self, ftype='form', title='', instructions=''): f = Form() f['type'] = ftype f['title'] = title f['instructions'] = instructions return f def handle_form(self, message): self.xmpp.event("message_xform", message) def build_form(self, xml): return Form(xml=xml) slixmpp-1.2.2/slixmpp/plugins/xep_0004/__init__.py0000644000175000001440000000066712770302340022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0004.stanza import Form from slixmpp.plugins.xep_0004.stanza import FormField, FieldOption from slixmpp.plugins.xep_0004.dataforms import XEP_0004 register_plugin(XEP_0004) slixmpp-1.2.2/slixmpp/plugins/xep_0004/stanza/0000755000175000001440000000000013014656513022114 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0004/stanza/__init__.py0000644000175000001440000000047412424504517024232 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.xep_0004.stanza.field import FormField, FieldOption from slixmpp.plugins.xep_0004.stanza.form import Form slixmpp-1.2.2/slixmpp/plugins/xep_0004/stanza/form.py0000644000175000001440000002060213004224717023425 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import copy import logging from collections import OrderedDict from slixmpp.thirdparty import OrderedSet from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins.xep_0004.stanza import FormField log = logging.getLogger(__name__) class Form(ElementBase): namespace = 'jabber:x:data' name = 'x' plugin_attrib = 'form' interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', )) sub_interfaces = {'title'} form_types = {'cancel', 'form', 'result', 'submit'} def __init__(self, *args, **kwargs): title = None if 'title' in kwargs: title = kwargs['title'] del kwargs['title'] ElementBase.__init__(self, *args, **kwargs) if title is not None: self['title'] = title def setup(self, xml=None): if ElementBase.setup(self, xml): # If we had to generate xml self['type'] = 'form' @property def field(self): return self.get_fields() def set_type(self, ftype): self._set_attr('type', ftype) if ftype == 'submit': fields = self.get_fields() for var in fields: field = fields[var] del field['type'] del field['label'] del field['desc'] del field['required'] del field['options'] elif ftype == 'cancel': del self['fields'] def add_field(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype field = FormField() field['var'] = var field['type'] = kwtype field['value'] = value if self['type'] in ('form', 'result'): field['label'] = label field['desc'] = desc field['required'] = required if options is not None: for option in options: field.add_option(**option) else: del field['type'] self.append(field) return field def add_item(self, values): itemXML = ET.Element('{%s}item' % self.namespace) self.xml.append(itemXML) reported_vars = self['reported'].keys() for var in reported_vars: field = FormField() field._type = self['reported'][var]['type'] field['var'] = var field['value'] = values.get(var, None) itemXML.append(field.xml) def add_reported(self, var, ftype=None, label='', desc='', **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) self.xml.append(reported) fieldXML = ET.Element('{%s}field' % FormField.namespace) reported.append(fieldXML) field = FormField(xml=fieldXML) field['var'] = var field['type'] = kwtype field['label'] = label field['desc'] = desc return field def cancel(self): self['type'] = 'cancel' def del_fields(self): fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: self.xml.remove(fieldXML) def del_instructions(self): instsXML = self.xml.findall('{%s}instructions') for instXML in instsXML: self.xml.remove(instXML) def del_items(self): itemsXML = self.xml.find('{%s}item' % self.namespace) for itemXML in itemsXML: self.xml.remove(itemXML) def del_reported(self): reportedXML = self.xml.find('{%s}reported' % self.namespace) if reportedXML is not None: self.xml.remove(reportedXML) def get_fields(self, use_dict=False): fields = OrderedDict() for stanza in self['substanzas']: if isinstance(stanza, FormField): fields[stanza['var']] = stanza return fields def get_instructions(self): instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) def get_items(self): items = [] itemsXML = self.xml.findall('{%s}item' % self.namespace) for itemXML in itemsXML: item = OrderedDict() fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) item[field['var']] = field['value'] items.append(item) return items def get_reported(self): fields = OrderedDict() xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, FormField.namespace)) for field in xml: field = FormField(xml=field) fields[field['var']] = field return fields def get_values(self): values = OrderedDict() fields = self.get_fields() for var in fields: values[var] = fields[var]['value'] return values def reply(self): if self['type'] == 'form': self['type'] = 'submit' elif self['type'] == 'submit': self['type'] = 'result' def set_fields(self, fields): del self['fields'] if not isinstance(fields, list): fields = fields.items() for var, field in fields: field['var'] = var self.add_field( var=field.get('var'), label=field.get('label'), desc=field.get('desc'), required=field.get('required'), value=field.get('value'), options=field.get('options'), type=field.get('type')) def set_instructions(self, instructions): del self['instructions'] if instructions in [None, '']: return if not isinstance(instructions, list): instructions = instructions.split('\n') for instruction in instructions: inst = ET.Element('{%s}instructions' % self.namespace) inst.text = instruction self.xml.append(inst) def set_items(self, items): for item in items: self.add_item(item) def set_reported(self, reported): """ This either needs a dictionary of dictionaries or a dictionary of form fields. :param reported: :return: """ for var in reported: field = reported[var] if isinstance(field, dict): self.add_reported(**field) else: reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) self.xml.append(reported) fieldXML = ET.Element('{%s}field' % FormField.namespace) reported.append(fieldXML) new_field = FormField(xml=fieldXML) new_field.values = field.values def set_values(self, values): fields = self.get_fields() for field in values: if field not in self.get_fields(): fields[field] = self.add_field(var=field) self.get_fields()[field]['value'] = values[field] def merge(self, other): new = copy.copy(self) if type(other) == dict: new['values'] = other return new nfields = new['fields'] ofields = other['fields'] nfields.update(ofields) new['fields'] = nfields return new Form.addField = Form.add_field Form.addReported = Form.add_reported Form.delFields = Form.del_fields Form.delInstructions = Form.del_instructions Form.delReported = Form.del_reported Form.getFields = Form.get_fields Form.getInstructions = Form.get_instructions Form.getReported = Form.get_reported Form.getValues = Form.get_values Form.setFields = Form.set_fields Form.setInstructions = Form.set_instructions Form.setReported = Form.set_reported Form.setValues = Form.set_values slixmpp-1.2.2/slixmpp/plugins/xep_0004/stanza/field.py0000644000175000001440000001377313004224717023560 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class FormField(ElementBase): namespace = 'jabber:x:data' name = 'field' plugin_attrib = 'field' plugin_multi_attrib = 'fields' interfaces = {'answer', 'desc', 'required', 'value', 'label', 'type', 'var'} sub_interfaces = {'desc'} plugin_tag_map = {} plugin_attrib_map = {} field_types = {'boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single'} true_values = {True, '1', 'true'} option_types = {'list-multi', 'list-single'} multi_line_types = {'hidden', 'text-multi'} multi_value_types = {'hidden', 'jid-multi', 'list-multi', 'text-multi'} def setup(self, xml=None): if ElementBase.setup(self, xml): self._type = None else: self._type = self['type'] def set_type(self, value): self._set_attr('type', value) if value: self._type = value def add_option(self, label='', value=''): if self._type is None or self._type in self.option_types: opt = FieldOption() opt['label'] = label opt['value'] = value self.append(opt) else: raise ValueError("Cannot add options to " + \ "a %s field." % self['type']) def del_options(self): optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: self.xml.remove(optXML) def del_required(self): reqXML = self.xml.find('{%s}required' % self.namespace) if reqXML is not None: self.xml.remove(reqXML) def del_value(self): valsXML = self.xml.findall('{%s}value' % self.namespace) for valXML in valsXML: self.xml.remove(valXML) def get_answer(self): return self['value'] def get_options(self): options = [] optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: opt = FieldOption(xml=optXML) options.append({'label': opt['label'], 'value': opt['value']}) return options def get_required(self): reqXML = self.xml.find('{%s}required' % self.namespace) return reqXML is not None def get_value(self, convert=True): valsXML = self.xml.findall('{%s}value' % self.namespace) if len(valsXML) == 0: return None elif self._type == 'boolean': if convert: return valsXML[0].text in self.true_values return valsXML[0].text elif self._type in self.multi_value_types or len(valsXML) > 1: values = [] for valXML in valsXML: if valXML.text is None: valXML.text = '' values.append(valXML.text) if self._type == 'text-multi' and convert: values = "\n".join(values) return values else: if valsXML[0].text is None: return '' return valsXML[0].text def set_answer(self, answer): self['value'] = answer def set_false(self): self['value'] = False def set_options(self, options): for value in options: if isinstance(value, dict): self.add_option(**value) else: self.add_option(value=value) def set_required(self, required): exists = self['required'] if not exists and required: self.xml.append(ET.Element('{%s}required' % self.namespace)) elif exists and not required: del self['required'] def set_true(self): self['value'] = True def set_value(self, value): del self['value'] valXMLName = '{%s}value' % self.namespace if self._type == 'boolean': if value in self.true_values: valXML = ET.Element(valXMLName) valXML.text = '1' self.xml.append(valXML) else: valXML = ET.Element(valXMLName) valXML.text = '0' self.xml.append(valXML) elif self._type in self.multi_value_types or self._type in ('', None): if isinstance(value, bool): value = [value] if not isinstance(value, list): value = value.replace('\r', '') value = value.split('\n') for val in value: if self._type in ('', None) and val in self.true_values: val = '1' valXML = ET.Element(valXMLName) valXML.text = val self.xml.append(valXML) else: if isinstance(value, list): raise ValueError("Cannot add multiple values " + \ "to a %s field." % self._type) valXML = ET.Element(valXMLName) valXML.text = value self.xml.append(valXML) class FieldOption(ElementBase): namespace = 'jabber:x:data' name = 'option' plugin_attrib = 'option' interfaces = {'label', 'value'} sub_interfaces = {'value'} plugin_multi_attrib = 'options' FormField.addOption = FormField.add_option FormField.delOptions = FormField.del_options FormField.delRequired = FormField.del_required FormField.delValue = FormField.del_value FormField.getAnswer = FormField.get_answer FormField.getOptions = FormField.get_options FormField.getRequired = FormField.get_required FormField.getValue = FormField.get_value FormField.setAnswer = FormField.set_answer FormField.setFalse = FormField.set_false FormField.setOptions = FormField.set_options FormField.setRequired = FormField.set_required FormField.setTrue = FormField.set_true FormField.setValue = FormField.set_value slixmpp-1.2.2/slixmpp/plugins/xep_0203/0000755000175000001440000000000013014656513020615 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0203/delay.py0000644000175000001440000000210512424504520022255 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0203 import stanza class XEP_0203(BasePlugin): """ XEP-0203: Delayed Delivery XMPP stanzas are sometimes withheld for delivery due to the recipient being offline, or are resent in order to establish recent history as is the case with MUCS. In any case, it is important to know when the stanza was originally sent, not just when it was last received. Also see . """ name = 'xep_0203' description = 'XEP-0203: Delayed Delivery' dependencies = set() stanza = stanza def plugin_init(self): """Start the XEP-0203 plugin.""" register_stanza_plugin(Message, stanza.Delay) register_stanza_plugin(Presence, stanza.Delay) slixmpp-1.2.2/slixmpp/plugins/xep_0203/__init__.py0000644000175000001440000000063512770302340022724 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0203 import stanza from slixmpp.plugins.xep_0203.stanza import Delay from slixmpp.plugins.xep_0203.delay import XEP_0203 register_plugin(XEP_0203) slixmpp-1.2.2/slixmpp/plugins/xep_0203/stanza.py0000644000175000001440000000214413004224717022464 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class Delay(ElementBase): name = 'delay' namespace = 'urn:xmpp:delay' plugin_attrib = 'delay' interfaces = {'from', 'stamp', 'text'} def get_from(self): from_ = self._get_attr('from') return JID(from_) if from_ else None def set_from(self, value): self._set_attr('from', str(value)) def get_stamp(self): timestamp = self._get_attr('stamp') return xep_0082.parse(timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('stamp', value) def get_text(self): return self.xml.text def set_text(self, value): self.xml.text = value def del_text(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/plugins/xep_0319/0000755000175000001440000000000013014656513020625 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0319/__init__.py0000644000175000001440000000063312424504520022732 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0319 import stanza from slixmpp.plugins.xep_0319.stanza import Idle from slixmpp.plugins.xep_0319.idle import XEP_0319 register_plugin(XEP_0319) slixmpp-1.2.2/slixmpp/plugins/xep_0319/idle.py0000644000175000001440000000467613004224717022125 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from datetime import datetime, timedelta from slixmpp.stanza import Presence from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0319 import stanza class XEP_0319(BasePlugin): name = 'xep_0319' description = 'XEP-0319: Last User Interaction in Presence' dependencies = {'xep_0012'} stanza = stanza def plugin_init(self): self._idle_stamps = {} register_stanza_plugin(Presence, stanza.Idle) self.api.register(self._set_idle, 'set_idle', default=True) self.api.register(self._get_idle, 'get_idle', default=True) self.xmpp.register_handler( Callback('Idle Presence', StanzaPath('presence/idle'), self._idle_presence)) self.xmpp.add_filter('out', self._stamp_idle_presence) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:1') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:1') self.xmpp.del_filter('out', self._stamp_idle_presence) self.xmpp.remove_handler('Idle Presence') def idle(self, jid=None, since=None): seconds = None if since is None: since = datetime.now() else: seconds = datetime.now() - since self.api['set_idle'](jid, None, None, since) self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) def active(self, jid=None): self.api['set_idle'](jid, None, None, None) self.xmpp['xep_0012'].del_last_activity(jid) def _set_idle(self, jid, node, ifrom, data): self._idle_stamps[jid] = data def _get_idle(self, jid, node, ifrom, data): return self._idle_stamps.get(jid, None) def _idle_presence(self, pres): self.xmpp.event('presence_idle', pres) def _stamp_idle_presence(self, stanza): if isinstance(stanza, Presence): since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid) if since: stanza['idle']['since'] = since return stanza slixmpp-1.2.2/slixmpp/plugins/xep_0319/stanza.py0000644000175000001440000000127613004224717022501 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class Idle(ElementBase): name = 'idle' namespace = 'urn:xmpp:idle:1' plugin_attrib = 'idle' interfaces = {'since'} def get_since(self): timestamp = self._get_attr('since') return xep_0082.parse(timestamp) def set_since(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('since', value) slixmpp-1.2.2/slixmpp/plugins/xep_0258/0000755000175000001440000000000013014656513020627 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0258/security_labels.py0000644000175000001440000000231613004224717024370 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq, Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0258 import stanza, SecurityLabel, Catalog log = logging.getLogger(__name__) class XEP_0258(BasePlugin): name = 'xep_0258' description = 'XEP-0258: Security Labels in XMPP' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, SecurityLabel) register_stanza_plugin(Iq, Catalog) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=SecurityLabel.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(SecurityLabel.namespace) def get_catalog(self, jid, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq() iq['to'] = jid iq['from'] = ifrom iq['type'] = 'get' iq.enable('security_label_catalog') return iq.send(callback=callback, timeout=timeout) slixmpp-1.2.2/slixmpp/plugins/xep_0258/__init__.py0000644000175000001440000000111512424504520022730 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0258 import stanza from slixmpp.plugins.xep_0258.stanza import SecurityLabel, Label from slixmpp.plugins.xep_0258.stanza import DisplayMarking, EquivalentLabel from slixmpp.plugins.xep_0258.stanza import ESSLabel, Catalog, CatalogItem from slixmpp.plugins.xep_0258.security_labels import XEP_0258 register_plugin(XEP_0258) slixmpp-1.2.2/slixmpp/plugins/xep_0258/stanza.py0000644000175000001440000000726613004225501022500 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from base64 import b64encode, b64decode from slixmpp import JID from slixmpp.util import bytes from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class SecurityLabel(ElementBase): name = 'securitylabel' namespace = 'urn:xmpp:sec-label:0' plugin_attrib = 'security_label' def add_equivalent(self, label): equiv = EquivalentLabel(parent=self) equiv.append(label) return equiv class Label(ElementBase): name = 'label' namespace = 'urn:xmpp:sec-label:0' plugin_attrib = 'label' class DisplayMarking(ElementBase): name = 'displaymarking' namespace = 'urn:xmpp:sec-label:0' plugin_attrib = 'display_marking' interfaces = {'fgcolor', 'bgcolor', 'value'} def get_fgcolor(self): return self._get_attr('fgcolor', 'black') def get_bgcolor(self): return self._get_attr('fgcolor', 'white') def get_value(self): return self.xml.text def set_value(self, value): self.xml.text = value def del_value(self): self.xml.text = '' class EquivalentLabel(ElementBase): name = 'equivalentlabel' namespace = 'urn:xmpp:sec-label:0' plugin_attrib = 'equivalent_label' plugin_multi_attrib = 'equivalent_labels' class Catalog(ElementBase): name = 'catalog' namespace = 'urn:xmpp:sec-label:catalog:2' plugin_attrib = 'security_label_catalog' interfaces = {'to', 'from', 'name', 'desc', 'id', 'size', 'restrict'} def get_to(self): return JID(self._get_attr('to')) pass def set_to(self, value): return self._set_attr('to', str(value)) def get_from(self): return JID(self._get_attr('from')) def set_from(self, value): return self._set_attr('from', str(value)) def get_restrict(self): value = self._get_attr('restrict', '') if value and value.lower() in ('true', '1'): return True return False def set_restrict(self, value): self._del_attr('restrict') if value: self._set_attr('restrict', 'true') elif value is False: self._set_attr('restrict', 'false') class CatalogItem(ElementBase): name = 'catalog' namespace = 'urn:xmpp:sec-label:catalog:2' plugin_attrib = 'item' plugin_multi_attrib = 'items' interfaces = {'selector', 'default'} def get_default(self): value = self._get_attr('default', '') if value.lower() in ('true', '1'): return True return False def set_default(self, value): self._del_attr('default') if value: self._set_attr('default', 'true') elif value is False: self._set_attr('default', 'false') class ESSLabel(ElementBase): name = 'esssecuritylabel' namespace = 'urn:xmpp:sec-label:ess:0' plugin_attrib = 'ess' interfaces = {'value'} def get_value(self): if self.xml.text: return b64decode(bytes(self.xml.text)) return '' def set_value(self, value): self.xml.text = '' if value: self.xml.text = b64encode(bytes(value)) def del_value(self): self.xml.text = '' register_stanza_plugin(Catalog, CatalogItem, iterable=True) register_stanza_plugin(CatalogItem, SecurityLabel) register_stanza_plugin(EquivalentLabel, ESSLabel) register_stanza_plugin(Label, ESSLabel) register_stanza_plugin(SecurityLabel, DisplayMarking) register_stanza_plugin(SecurityLabel, EquivalentLabel, iterable=True) register_stanza_plugin(SecurityLabel, Label) slixmpp-1.2.2/slixmpp/plugins/xep_0045.1.py0000644000175000001440000003653312736764614021360 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import with_statement import logging from slixmpp import Presence from slixmpp.plugins import BasePlugin, register_plugin from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.xpath import MatchXPath from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask from slixmpp.exceptions import IqError, IqTimeout log = logging.getLogger(__name__) class MUCPresence(ElementBase): name = 'x' namespace = 'http://jabber.org/protocol/muc#user' plugin_attrib = 'muc' interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room')) affiliations = set(('', )) roles = set(('', )) def getXMLItem(self): item = self.xml.find('{http://jabber.org/protocol/muc#user}item') if item is None: item = ET.Element('{http://jabber.org/protocol/muc#user}item') self.xml.append(item) return item def getAffiliation(self): #TODO if no affilation, set it to the default and return default item = self.getXMLItem() return item.get('affiliation', '') def setAffiliation(self, value): item = self.getXMLItem() #TODO check for valid affiliation item.attrib['affiliation'] = value return self def delAffiliation(self): item = self.getXMLItem() #TODO set default affiliation if 'affiliation' in item.attrib: del item.attrib['affiliation'] return self def getJid(self): item = self.getXMLItem() return JID(item.get('jid', '')) def setJid(self, value): item = self.getXMLItem() if not isinstance(value, str): value = str(value) item.attrib['jid'] = value return self def delJid(self): item = self.getXMLItem() if 'jid' in item.attrib: del item.attrib['jid'] return self def getRole(self): item = self.getXMLItem() #TODO get default role, set default role if none return item.get('role', '') def setRole(self, value): item = self.getXMLItem() #TODO check for valid role item.attrib['role'] = value return self def delRole(self): item = self.getXMLItem() #TODO set default role if 'role' in item.attrib: del item.attrib['role'] return self def getNick(self): return self.parent()['from'].resource def getRoom(self): return self.parent()['from'].bare def setNick(self, value): log.warning("Cannot set nick through mucpresence plugin.") return self def setRoom(self, value): log.warning("Cannot set room through mucpresence plugin.") return self def delNick(self): log.warning("Cannot delete nick through mucpresence plugin.") return self def delRoom(self): log.warning("Cannot delete room through mucpresence plugin.") return self class XEP_0045(BasePlugin): """ Implements XEP-0045 Multi-User Chat """ name = 'xep_0045' description = 'XEP-0045: Multi-User Chat' dependencies = set(['xep_0030', 'xep_0004']) def plugin_init(self): self.rooms = {} self.ourNicks = {} self.xep = '0045' # load MUC support in presence stanzas register_stanza_plugin(Presence, MUCPresence) self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_error_message)) self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_message)) self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_subject)) self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("" % self.xmpp.default_ns), self.handle_config_change)) self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( self.xmpp.default_ns, 'http://jabber.org/protocol/muc#user', 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite)) def plugin_end(self): self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc') def handle_groupchat_invite(self, inv): """ Handle an invite into a muc. """ logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv) if inv['from'] not in self.rooms.keys(): self.xmpp.event("groupchat_invite", inv) def handle_config_change(self, msg): """Handle a MUC configuration change (with status code).""" self.xmpp.event('groupchat_config_status', msg) self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg) def handle_groupchat_presence(self, pr): """ Handle a presence in a muc. """ got_offline = False got_online = False if pr['muc']['room'] not in self.rooms.keys(): return entry = pr['muc'].get_stanza_values() entry['show'] = pr['show'] entry['status'] = pr['status'] entry['alt_nick'] = pr['nick'] if pr['type'] == 'unavailable': if entry['nick'] in self.rooms[entry['room']]: del self.rooms[entry['room']][entry['nick']] got_offline = True else: if entry['nick'] not in self.rooms[entry['room']]: got_online = True self.rooms[entry['room']][entry['nick']] = entry log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry) self.xmpp.event("groupchat_presence", pr) self.xmpp.event("muc::%s::presence" % entry['room'], pr) if got_offline: self.xmpp.event("muc::%s::got_offline" % entry['room'], pr) if got_online: self.xmpp.event("muc::%s::got_online" % entry['room'], pr) def handle_groupchat_message(self, msg): """ Handle a message event in a muc. """ self.xmpp.event('groupchat_message', msg) self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) def handle_groupchat_error_message(self, msg): """ Handle a message error event in a muc. """ self.xmpp.event('groupchat_message_error', msg) self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg) def handle_groupchat_subject(self, msg): """ Handle a message coming from a muc indicating a change of subject (or announcing it when joining the room) """ self.xmpp.event('groupchat_subject', msg) def jidInRoom(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return True return False def getNick(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return nick def configureRoom(self, room, form=None, ifrom=None): if form is None: form = self.getRoomConfig(room, ifrom=ifrom) iq = self.xmpp.make_iq_set() iq['to'] = room if ifrom is not None: iq['from'] = ifrom query = ET.Element('{http://jabber.org/protocol/muc#owner}query') form = form.getXML('submit') query.append(form) iq.append(query) # For now, swallow errors to preserve existing API try: result = iq.send() except IqError: return False except IqTimeout: return False return True 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.make_presence(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('{http://jabber.org/protocol/muc}password') passelement.text = password x.append(passelement) if maxhistory: history = ET.Element('{http://jabber.org/protocol/muc}history') if maxhistory == "0": history.attrib['maxchars'] = maxhistory else: history.attrib['maxstanzas'] = maxhistory x.append(history) stanza.append(x) if not wait: self.xmpp.send(stanza) else: #wait for our own room presence back expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)}) self.xmpp.send(stanza, expect) self.rooms[room] = {} self.ourNicks[room] = nick def destroy(self, room, reason='', altroom = '', ifrom=None): iq = self.xmpp.make_iq_set() if ifrom is not None: iq['from'] = ifrom iq['to'] = room query = ET.Element('{http://jabber.org/protocol/muc#owner}query') destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy') if altroom: destroy.attrib['jid'] = altroom xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason') xreason.text = reason destroy.append(xreason) query.append(destroy) iq.append(query) # For now, swallow errors to preserve existing API try: r = iq.send() except IqError: return False except IqTimeout: return False return True 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 query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if nick is not None: item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) else: item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid}) query.append(item) iq = self.xmpp.make_iq_set(query) iq['to'] = room iq['from'] = ifrom # For now, swallow errors to preserve existing API try: result = iq.send() except IqError: return False except IqTimeout: return False return True def setRole(self, room, nick, role): """ Change role property of a nick in a room. Typically, roles are temporary (they last only as long as you are in the room), whereas affiliations are permanent (they last across groupchat sessions). """ if role not in ('moderator', 'participant', 'visitor', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') item = ET.Element('item', {'role':role, 'nick':nick}) query.append(item) iq = self.xmpp.make_iq_set(query) iq['to'] = room result = iq.send() if result is False or result['type'] != 'result': raise ValueError return True def invite(self, room, jid, reason='', mfrom=''): """ Invite a jid to a room.""" msg = self.xmpp.make_message(room) msg['from'] = mfrom x = ET.Element('{http://jabber.org/protocol/muc#user}x') invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) if reason: rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason') rxml.text = reason invite.append(rxml) x.append(invite) msg.append(x) self.xmpp.send(msg) def leaveMUC(self, room, nick, msg='', pfrom=None): """ Leave the specified room. """ if msg: self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom) else: self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom) del self.rooms[room] def getRoomConfig(self, room, ifrom=''): iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner') iq['to'] = room iq['from'] = ifrom # For now, swallow errors to preserve existing API try: result = iq.send() 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: raise ValueError return self.xmpp.plugin['xep_0004'].buildForm(form) 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.make_iq_set(query) iq['to'] = room iq['from'] = ifrom iq.send() def setRoomConfig(self, room, config, ifrom=''): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') x = config.getXML('submit') query.append(x) iq = self.xmpp.make_iq_set(query) iq['to'] = room iq['from'] = ifrom iq.send() def getJoinedRooms(self): return self.rooms.keys() def getOurJidInRoom(self, roomJid): """ Return the jid we're using in a room. """ return "%s/%s" % (roomJid, self.ourNicks[roomJid]) def getJidProperty(self, room, nick, jidProperty): """ Get the property of a nick in a room, such as its 'jid' or 'affiliation' If not found, return None. """ if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]: return self.rooms[room][nick][jidProperty] else: return None def getRoster(self, room): """ Get the list of nicks in a room. """ if room not in self.rooms.keys(): return None return self.rooms[room].keys() def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None): if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation}) query.append(item) iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get') iq.append(query) return iq.send() xep_0045 = XEP_0045 register_plugin(XEP_0045) slixmpp-1.2.2/slixmpp/plugins/xep_0242.py0000644000175000001440000000102013004224717021157 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins import BasePlugin, register_plugin class XEP_0242(BasePlugin): name = 'xep_0242' description = 'XEP-0242: XMPP Client Compliance 2009' dependencies = {'xep_0030', 'xep_0115', 'xep_0054', 'xep_0045', 'xep_0085', 'xep_0016', 'xep_0191'} register_plugin(XEP_0242) slixmpp-1.2.2/slixmpp/plugins/xep_0078/0000755000175000001440000000000013014656513020627 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0078/__init__.py0000644000175000001440000000066012770302340022734 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0078 import stanza from slixmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature from slixmpp.plugins.xep_0078.legacyauth import XEP_0078 register_plugin(XEP_0078) slixmpp-1.2.2/slixmpp/plugins/xep_0078/legacyauth.py0000644000175000001440000001032312572154127023330 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import uuid import logging import hashlib from slixmpp.jid import JID from slixmpp.exceptions import IqError, IqTimeout from slixmpp.stanza import Iq, StreamFeatures from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0078 import stanza log = logging.getLogger(__name__) class XEP_0078(BasePlugin): """ XEP-0078 NON-SASL Authentication This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin unless you are forced to use an old XMPP server implementation. """ name = 'xep_0078' description = 'XEP-0078: Non-SASL Authentication' dependencies = set() stanza = stanza default_config = { 'order': 15 } def plugin_init(self): self.xmpp.register_feature('auth', self._handle_auth, restart=False, order=self.order) self.xmpp.add_event_handler('legacy_protocol', self._handle_legacy_protocol) register_stanza_plugin(Iq, stanza.IqAuth) register_stanza_plugin(StreamFeatures, stanza.AuthFeature) def plugin_end(self): self.xmpp.del_event_handler('legacy_protocol', self._handle_legacy_protocol) self.xmpp.unregister_feature('auth', self.order) def _handle_auth(self, features): # If we can or have already authenticated with SASL, do nothing. if 'mechanisms' in features['features']: return False return self.authenticate() def _handle_legacy_protocol(self, event): self.authenticate() def authenticate(self): if self.xmpp.authenticated: return False log.debug("Starting jabber:iq:auth Authentication") # Step 1: Request the auth form iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = self.xmpp.requested_jid.host iq['auth']['username'] = self.xmpp.requested_jid.user try: resp = iq.send() except IqError as err: log.info("Authentication failed: %s", err.iq['error']['condition']) self.xmpp.event('failed_auth') self.xmpp.disconnect() return True except IqTimeout: log.info("Authentication failed: %s", 'timeout') self.xmpp.event('failed_auth') self.xmpp.disconnect() return True # Step 2: Fill out auth form for either password or digest auth iq = self.xmpp.Iq() iq['type'] = 'set' iq['auth']['username'] = self.xmpp.requested_jid.user # A resource is required, so create a random one if necessary resource = self.xmpp.requested_jid.resource if not resource: resource = str(uuid.uuid4()) iq['auth']['resource'] = resource if 'digest' in resp['auth']['fields']: log.debug('Authenticating via jabber:iq:auth Digest') stream_id = bytes(self.xmpp.stream_id, encoding='utf-8') password = bytes(self.xmpp.password, encoding='utf-8') digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest() iq['auth']['digest'] = digest else: log.warning('Authenticating via jabber:iq:auth Plain.') iq['auth']['password'] = self.xmpp.password # Step 3: Send credentials try: result = iq.send() except IqError as err: log.info("Authentication failed") self.xmpp.event("failed_auth") self.xmpp.disconnect() except IqTimeout: log.info("Authentication failed") self.xmpp.event("failed_auth") self.xmpp.disconnect() self.xmpp.features.add('auth') self.xmpp.authenticated = True self.xmpp.boundjid = JID(self.xmpp.requested_jid) self.xmpp.boundjid.resource = resource self.xmpp.event('session_bind', self.xmpp.boundjid) log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') return True slixmpp-1.2.2/slixmpp/plugins/xep_0078/stanza.py0000644000175000001440000000221013004224717022470 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class IqAuth(ElementBase): namespace = 'jabber:iq:auth' name = 'query' plugin_attrib = 'auth' interfaces = {'fields', 'username', 'password', 'resource', 'digest'} sub_interfaces = {'username', 'password', 'resource', 'digest'} plugin_tag_map = {} plugin_attrib_map = {} def get_fields(self): fields = set() for field in self.sub_interfaces: if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: fields.add(field) return fields def set_resource(self, value): self._set_sub_text('resource', value, keep=True) def set_password(self, value): self._set_sub_text('password', value, keep=True) class AuthFeature(ElementBase): namespace = 'http://jabber.org/features/iq-auth' name = 'auth' plugin_attrib = 'auth' interfaces = set() plugin_tag_map = {} plugin_attrib_map = {} slixmpp-1.2.2/slixmpp/plugins/xep_0221/0000755000175000001440000000000013014656513020615 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0221/__init__.py0000644000175000001440000000064212424504520022722 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0221 import stanza from slixmpp.plugins.xep_0221.stanza import Media, URI from slixmpp.plugins.xep_0221.media import XEP_0221 register_plugin(XEP_0221) slixmpp-1.2.2/slixmpp/plugins/xep_0221/media.py0000644000175000001440000000121513004224717022241 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0221 import stanza, Media, URI from slixmpp.plugins.xep_0004 import FormField log = logging.getLogger(__name__) class XEP_0221(BasePlugin): name = 'xep_0221' description = 'XEP-0221: Data Forms Media Element' dependencies = {'xep_0004'} def plugin_init(self): register_stanza_plugin(FormField, Media) slixmpp-1.2.2/slixmpp/plugins/xep_0221/stanza.py0000644000175000001440000000167413004224765022476 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Media(ElementBase): name = 'media' namespace = 'urn:xmpp:media-element' plugin_attrib = 'media' interfaces = {'height', 'width', 'alt'} def add_uri(self, value, itype): uri = URI() uri['value'] = value uri['type'] = itype self.append(uri) class URI(ElementBase): name = 'uri' namespace = 'urn:xmpp:media-element' plugin_attrib = 'uri' plugin_multi_attrib = 'uris' interfaces = {'type', 'value'} def get_value(self): return self.xml.text def set_value(self, value): self.xml.text = value def del_value(self): self.xml.text = '' register_stanza_plugin(Media, URI, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0131/0000755000175000001440000000000013014656513020615 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0131/__init__.py0000644000175000001440000000064112424504520022721 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0131 import stanza from slixmpp.plugins.xep_0131.stanza import Headers from slixmpp.plugins.xep_0131.headers import XEP_0131 register_plugin(XEP_0131) slixmpp-1.2.2/slixmpp/plugins/xep_0131/headers.py0000644000175000001440000000241513004224717022600 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0131 import stanza from slixmpp.plugins.xep_0131.stanza import Headers class XEP_0131(BasePlugin): name = 'xep_0131' description = 'XEP-0131: Stanza Headers and Internet Metadata' dependencies = {'xep_0030'} stanza = stanza default_config = { 'supported_headers': set() } def plugin_init(self): register_stanza_plugin(Message, Headers) register_stanza_plugin(Presence, Headers) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Headers.namespace) for header in self.supported_headers: self.xmpp['xep_0030'].del_feature( feature='%s#%s' % (Headers.namespace, header)) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Headers.namespace) for header in self.supported_headers: self.xmpp['xep_0030'].add_feature('%s#%s' % ( Headers.namespace, header)) slixmpp-1.2.2/slixmpp/plugins/xep_0131/stanza.py0000644000175000001440000000307313004224717022466 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from collections import OrderedDict from slixmpp.xmlstream import ET, ElementBase class Headers(ElementBase): name = 'headers' namespace = 'http://jabber.org/protocol/shim' plugin_attrib = 'headers' interfaces = {'headers'} is_extension = True def get_headers(self): result = OrderedDict() headers = self.xml.findall('{%s}header' % self.namespace) for header in headers: name = header.attrib.get('name', '') value = header.text if name in result: if not isinstance(result[name], set): result[name] = [result[name]] else: result[name] = [] result[name].add(value) else: result[name] = value return result def set_headers(self, values): self.del_headers() for name in values: vals = values[name] if not isinstance(vals, (list, set)): vals = [values[name]] for value in vals: header = ET.Element('{%s}header' % self.namespace) header.attrib['name'] = name header.text = value self.xml.append(header) def del_headers(self): headers = self.xml.findall('{%s}header' % self.namespace) for header in headers: self.xml.remove(header) slixmpp-1.2.2/slixmpp/plugins/xep_0079/0000755000175000001440000000000013014656513020630 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0079/__init__.py0000644000175000001440000000075412424504520022741 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0079.stanza import ( AMP, Rule, InvalidRules, UnsupportedConditions, UnsupportedActions, FailedRules, FailedRule, AMPFeature) from slixmpp.plugins.xep_0079.amp import XEP_0079 register_plugin(XEP_0079) slixmpp-1.2.2/slixmpp/plugins/xep_0079/amp.py0000644000175000001440000000477513004224717021770 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permissio """ import logging from slixmpp.stanza import Message, Error, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.matcher import StanzaPath, MatchMany from slixmpp.xmlstream.handler import Callback from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0079 import stanza log = logging.getLogger(__name__) class XEP_0079(BasePlugin): """ XEP-0079 Advanced Message Processing """ name = 'xep_0079' description = 'XEP-0079: Advanced Message Processing' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, stanza.AMP) register_stanza_plugin(Error, stanza.InvalidRules) register_stanza_plugin(Error, stanza.UnsupportedConditions) register_stanza_plugin(Error, stanza.UnsupportedActions) register_stanza_plugin(Error, stanza.FailedRules) self.xmpp.register_handler( Callback('AMP Response', MatchMany([ StanzaPath('message/error/failed_rules'), StanzaPath('message/amp') ]), self._handle_amp_response)) if not self.xmpp.is_component: self.xmpp.register_feature('amp', self._handle_amp_feature, restart=False, order=9000) register_stanza_plugin(StreamFeatures, stanza.AMPFeature) def plugin_end(self): self.xmpp.remove_handler('AMP Response') def _handle_amp_response(self, msg): log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') if msg['type'] == 'error': self.xmpp.event('amp_error', msg) elif msg['amp']['status'] in ('alert', 'notify'): self.xmpp.event('amp_%s' % msg['amp']['status'], msg) def _handle_amp_feature(self, features): log.debug('Advanced Message Processing is available.') self.xmpp.features.add('amp') def discover_support(self, jid=None, **iqargs): if jid is None: if self.xmpp.is_component: jid = self.xmpp.server_host else: jid = self.xmpp.boundjid.host return self.xmpp['xep_0030'].get_info( jid=jid, node='http://jabber.org/protocol/amp', **iqargs) slixmpp-1.2.2/slixmpp/plugins/xep_0079/stanza.py0000644000175000001440000000505613004225455022504 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import unicode_literals from slixmpp import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class AMP(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'amp' plugin_attrib = 'amp' interfaces = {'from', 'to', 'status', 'per_hop'} def get_from(self): return JID(self._get_attr('from')) def set_from(self, value): return self._set_attr('from', str(value)) def get_to(self): return JID(self._get_attr('from')) def set_to(self, value): return self._set_attr('to', str(value)) def get_per_hop(self): return self._get_attr('per-hop') == 'true' def set_per_hop(self, value): if value: return self._set_attr('per-hop', 'true') else: return self._del_attr('per-hop') def del_per_hop(self): return self._del_attr('per-hop') def add_rule(self, action, condition, value): rule = Rule(parent=self) rule['action'] = action rule['condition'] = condition rule['value'] = value class Rule(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'rule' plugin_attrib = name plugin_multi_attrib = 'rules' interfaces = {'action', 'condition', 'value'} class InvalidRules(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'invalid-rules' plugin_attrib = 'invalid_rules' class UnsupportedConditions(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'unsupported-conditions' plugin_attrib = 'unsupported_conditions' class UnsupportedActions(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'unsupported-actions' plugin_attrib = 'unsupported_actions' class FailedRule(Rule): namespace = 'http://jabber.org/protocol/amp#errors' class FailedRules(ElementBase): namespace = 'http://jabber.org/protocol/amp#errors' name = 'failed-rules' plugin_attrib = 'failed_rules' class AMPFeature(ElementBase): namespace = 'http://jabber.org/features/amp' name = 'amp' register_stanza_plugin(AMP, Rule, iterable=True) register_stanza_plugin(InvalidRules, Rule, iterable=True) register_stanza_plugin(UnsupportedConditions, Rule, iterable=True) register_stanza_plugin(UnsupportedActions, Rule, iterable=True) register_stanza_plugin(FailedRules, FailedRule, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0199/0000755000175000001440000000000013014656513020633 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0199/__init__.py0000644000175000001440000000053512770302340022741 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0199.stanza import Ping from slixmpp.plugins.xep_0199.ping import XEP_0199 register_plugin(XEP_0199) slixmpp-1.2.2/slixmpp/plugins/xep_0199/ping.py0000644000175000001440000001374513004224717022150 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import time import logging from slixmpp.jid import JID from slixmpp.stanza import Iq from slixmpp import asyncio from slixmpp.exceptions import IqError, IqTimeout from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.handler import Callback from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0199 import stanza, Ping log = logging.getLogger(__name__) class XEP_0199(BasePlugin): """ XEP-0199: XMPP Ping Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Also see . Attributes: keepalive -- If True, periodically send ping requests to the server. If a ping is not answered, the connection will be reset. interval -- Time in seconds between keepalive pings. Defaults to 300 seconds. timeout -- Time in seconds to wait for a ping response. Defaults to 30 seconds. Methods: send_ping -- Send a ping to a given JID, returning the round trip time. """ name = 'xep_0199' description = 'XEP-0199: XMPP Ping' dependencies = {'xep_0030'} stanza = stanza default_config = { 'keepalive': False, 'interval': 300, 'timeout': 30 } def plugin_init(self): """ Start the XEP-0199 plugin. """ register_stanza_plugin(Iq, Ping) self.xmpp.register_handler( Callback('Ping', StanzaPath('iq@type=get/ping'), self._handle_ping)) if self.keepalive: self.xmpp.add_event_handler('session_start', self.enable_keepalive) self.xmpp.add_event_handler('session_end', self.disable_keepalive) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Ping.namespace) self.xmpp.remove_handler('Ping') if self.keepalive: self.xmpp.del_event_handler('session_start', self.enable_keepalive) self.xmpp.del_event_handler('session_end', self.disable_keepalive) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Ping.namespace) def enable_keepalive(self, interval=None, timeout=None): if interval: self.interval = interval if timeout: self.timeout = timeout self.keepalive = True handler = lambda event=None: asyncio.ensure_future(self._keepalive(event)) self.xmpp.schedule('Ping keepalive', self.interval, handler, repeat=True) def disable_keepalive(self, event=None): self.xmpp.cancel_schedule('Ping keepalive') @asyncio.coroutine def _keepalive(self, event=None): log.debug("Keepalive ping...") try: rtt = yield from self.ping(self.xmpp.boundjid.host, timeout=self.timeout) except IqTimeout: log.debug("Did not recieve ping back in time." + \ "Requesting Reconnect.") self.xmpp.reconnect() else: log.debug('Keepalive RTT: %s' % rtt) def _handle_ping(self, iq): """Automatically reply to ping requests.""" log.debug("Pinged by %s", iq['from']) iq.reply().send() def send_ping(self, jid, ifrom=None, timeout=None, callback=None, timeout_callback=None): """Send a ping request. Arguments: jid -- The JID that will receive the ping. ifrom -- Specifiy the sender JID. timeout -- Time in seconds to wait for a response. Defaults to self.timeout. callback -- Optional handler to execute when a pong is received. """ if not timeout: timeout = self.timeout iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid iq['from'] = ifrom iq.enable('ping') return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) @asyncio.coroutine def ping(self, jid=None, ifrom=None, timeout=None): """Send a ping request and calculate RTT. This is a coroutine. Arguments: jid -- The JID that will receive the ping. ifrom -- Specifiy the sender JID. timeout -- Time in seconds to wait for a response. Defaults to self.timeout. """ own_host = False if not jid: if self.xmpp.is_component: jid = self.xmpp.server else: jid = self.xmpp.boundjid.host jid = JID(jid) if jid == self.xmpp.boundjid.host or \ self.xmpp.is_component and jid == self.xmpp.server: own_host = True if not timeout: timeout = self.timeout start = time.time() log.debug('Pinging %s' % jid) try: yield from self.send_ping(jid, ifrom=ifrom, timeout=timeout) except IqError as e: if own_host: rtt = time.time() - start log.debug('Pinged %s, RTT: %s', jid, rtt) return rtt else: raise e else: rtt = time.time() - start log.debug('Pinged %s, RTT: %s', jid, rtt) return rtt slixmpp-1.2.2/slixmpp/plugins/xep_0199/stanza.py0000644000175000001440000000144712424504520022505 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import slixmpp from slixmpp.xmlstream import ElementBase class Ping(ElementBase): """ Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Example ping stanza: Stanza Interface: None Methods: None """ name = 'ping' namespace = 'urn:xmpp:ping' plugin_attrib = 'ping' interfaces = set() slixmpp-1.2.2/slixmpp/plugins/xep_0184/0000755000175000001440000000000013014656513020625 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0184/receipt.py0000644000175000001440000000744713004224717022642 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0184 import stanza, Request, Received class XEP_0184(BasePlugin): """ XEP-0184: Message Delivery Receipts """ name = 'xep_0184' description = 'XEP-0184: Message Delivery Receipts' dependencies = {'xep_0030'} stanza = stanza default_config = { 'auto_ack': True, 'auto_request': False } ack_types = ('normal', 'chat', 'headline') def plugin_init(self): register_stanza_plugin(Message, Request) register_stanza_plugin(Message, Received) self.xmpp.add_filter('out', self._filter_add_receipt_request) self.xmpp.register_handler( Callback('Message Receipt', StanzaPath('message/receipt'), self._handle_receipt_received)) self.xmpp.register_handler( Callback('Message Receipt Request', StanzaPath('message/request_receipt'), self._handle_receipt_request)) def plugin_end(self): self.xmpp['xep_0030'].del_feature('urn:xmpp:receipts') self.xmpp.del_filter('out', self._filter_add_receipt_request) self.xmpp.remove_handler('Message Receipt') self.xmpp.remove_handler('Message Receipt Request') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts') def ack(self, msg): """ Acknowledge a message by sending a receipt. Arguments: msg -- The message to acknowledge. """ ack = self.xmpp.Message() ack['to'] = msg['from'] ack['receipt'] = msg['id'] ack.send() def _handle_receipt_received(self, msg): self.xmpp.event('receipt_received', msg) def _handle_receipt_request(self, msg): """ Auto-ack message receipt requests if ``self.auto_ack`` is ``True``. Arguments: msg -- The incoming message requesting a receipt. """ if self.auto_ack: if msg['type'] in self.ack_types: if not msg['receipt']: self.ack(msg) def _filter_add_receipt_request(self, stanza): """ Auto add receipt requests to outgoing messages, if: - ``self.auto_request`` is set to ``True`` - The message is not for groupchat - The message does not contain a receipt acknowledgment - The recipient is a bare JID or, if a full JID, one that has the ``urn:xmpp:receipts`` feature enabled The disco cache is checked if a full JID is specified in the outgoing message, which may mean a round-trip disco#info delay for the first message sent to the JID if entity caps are not used. """ if not self.auto_request: return stanza if not isinstance(stanza, Message): return stanza if stanza['request_receipt']: return stanza if not stanza['type'] in self.ack_types: return stanza if stanza['receipt']: return stanza if not stanza['body']: return stanza if stanza['to'].resource: if not self.xmpp['xep_0030'].supports(stanza['to'], feature='urn:xmpp:receipts', cached=True): return stanza stanza['request_receipt'] = True return stanza slixmpp-1.2.2/slixmpp/plugins/xep_0184/__init__.py0000644000175000001440000000060612770302340022732 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0184.stanza import Request, Received from slixmpp.plugins.xep_0184.receipt import XEP_0184 register_plugin(XEP_0184) slixmpp-1.2.2/slixmpp/plugins/xep_0184/stanza.py0000644000175000001440000000375113004224717022501 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream.stanzabase import ElementBase, ET class Request(ElementBase): namespace = 'urn:xmpp:receipts' name = 'request' plugin_attrib = 'request_receipt' interfaces = {'request_receipt'} sub_interfaces = interfaces is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_request_receipt(self, val): self.del_request_receipt() if val: parent = self.parent() parent._set_sub_text("{%s}request" % self.namespace, keep=True) if not parent['id']: if parent.stream: parent['id'] = parent.stream.new_id() def get_request_receipt(self): parent = self.parent() if parent.xml.find("{%s}request" % self.namespace) is not None: return True else: return False def del_request_receipt(self): self.parent()._del_sub("{%s}request" % self.namespace) class Received(ElementBase): namespace = 'urn:xmpp:receipts' name = 'received' plugin_attrib = 'receipt' interfaces = {'receipt'} sub_interfaces = interfaces is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_receipt(self, value): self.del_receipt() if value: parent = self.parent() xml = ET.Element("{%s}received" % self.namespace) xml.attrib['id'] = value parent.append(xml) def get_receipt(self): parent = self.parent() xml = parent.xml.find("{%s}received" % self.namespace) if xml is not None: return xml.attrib.get('id', '') return '' def del_receipt(self): self.parent()._del_sub('{%s}received' % self.namespace) slixmpp-1.2.2/slixmpp/plugins/xep_0138.py0000644000175000001440000001010613004224717021170 0ustar mathieuiusers00000000000000""" slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz This file is part of slixmpp. See the file LICENSE for copying permission. """ import logging import zlib from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import RestartStream, register_stanza_plugin, ElementBase, StanzaBase from slixmpp.xmlstream.matcher import * from slixmpp.xmlstream.handler import * from slixmpp.plugins import BasePlugin, register_plugin log = logging.getLogger(__name__) class Compression(ElementBase): name = 'compression' namespace = 'http://jabber.org/features/compress' interfaces = {'methods'} plugin_attrib = 'compression' plugin_tag_map = {} plugin_attrib_map = {} def get_methods(self): methods = [] for method in self.xml.findall('{%s}method' % self.namespace): methods.append(method.text) return methods class Compress(StanzaBase): name = 'compress' namespace = 'http://jabber.org/protocol/compress' interfaces = {'method'} sub_interfaces = interfaces plugin_attrib = 'compress' plugin_tag_map = {} plugin_attrib_map = {} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class Compressed(StanzaBase): name = 'compressed' namespace = 'http://jabber.org/protocol/compress' interfaces = set() plugin_tag_map = {} plugin_attrib_map = {} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class ZlibSocket(object): def __init__(self, socketobj): self.__socket = socketobj self.compressor = zlib.compressobj() self.decompressor = zlib.decompressobj(zlib.MAX_WBITS) def __getattr__(self, name): return getattr(self.__socket, name) def send(self, data): sentlen = len(data) data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) log.debug(b'>>> (compressed)' + (data.encode("hex"))) #return self.__socket.send(data) sentactuallen = self.__socket.send(data) assert(sentactuallen == len(data)) return sentlen def recv(self, *args, **kwargs): data = self.__socket.recv(*args, **kwargs) log.debug(b'<<< (compressed)' + data.encode("hex")) return self.decompressor.decompress(self.decompressor.unconsumed_tail + data) class XEP_0138(BasePlugin): """ XEP-0138: Compression """ name = "xep_0138" description = "XEP-0138: Compression" dependencies = {"xep_0030"} def plugin_init(self): self.xep = '0138' self.description = 'Stream Compression (Generic)' self.compression_methods = {'zlib': True} register_stanza_plugin(StreamFeatures, Compression) self.xmpp.register_stanza(Compress) self.xmpp.register_stanza(Compressed) self.xmpp.register_handler( Callback('Compressed', StanzaPath('compressed'), self._handle_compressed, instream=True)) self.xmpp.register_feature('compression', self._handle_compression, restart=True, order=self.config.get('order', 5)) def register_compression_method(self, name, handler): self.compression_methods[name] = handler def _handle_compression(self, features): for method in features['compression']['methods']: if method in self.compression_methods: log.info('Attempting to use %s compression' % method) c = Compress(self.xmpp) c['method'] = method c.send(now=True) return True return False def _handle_compressed(self, stanza): self.xmpp.features.add('compression') log.debug('Stream Compressed!') compressed_socket = ZlibSocket(self.xmpp.socket) self.xmpp.set_socket(compressed_socket) raise RestartStream() def _handle_failure(self, stanza): pass xep_0138 = XEP_0138 register_plugin(XEP_0138) slixmpp-1.2.2/slixmpp/plugins/xep_0198/0000755000175000001440000000000013014656513020632 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0198/__init__.py0000644000175000001440000000116012424504520022733 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0198.stanza import Enable, Enabled from slixmpp.plugins.xep_0198.stanza import Resume, Resumed from slixmpp.plugins.xep_0198.stanza import Failed from slixmpp.plugins.xep_0198.stanza import StreamManagement from slixmpp.plugins.xep_0198.stanza import Ack, RequestAck from slixmpp.plugins.xep_0198.stream_management import XEP_0198 register_plugin(XEP_0198) slixmpp-1.2.2/slixmpp/plugins/xep_0198/stream_management.py0000644000175000001440000002600012724635506024677 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import asyncio import logging import collections from slixmpp.stanza import Message, Presence, Iq, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback, Waiter from slixmpp.xmlstream.matcher import MatchXPath, MatchMany from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0198 import stanza log = logging.getLogger(__name__) MAX_SEQ = 2 ** 32 class XEP_0198(BasePlugin): """ XEP-0198: Stream Management """ name = 'xep_0198' description = 'XEP-0198: Stream Management' dependencies = set() stanza = stanza default_config = { #: The last ack number received from the server. 'last_ack': 0, #: The number of stanzas to wait between sending ack requests to #: the server. Setting this to ``1`` will send an ack request after #: every sent stanza. Defaults to ``5``. 'window': 5, #: The stream management ID for the stream. Knowing this value is #: required in order to do stream resumption. 'sm_id': None, #: A counter of handled incoming stanzas, mod 2^32. 'handled': 0, #: A counter of unacked outgoing stanzas, mod 2^32. 'seq': 0, #: Control whether or not the ability to resume the stream will be #: requested when enabling stream management. Defaults to ``True``. 'allow_resume': True, 'order': 10100, 'resume_order': 9000 } def plugin_init(self): """Start the XEP-0198 plugin.""" # Only enable stream management for non-components, # since components do not yet perform feature negotiation. if self.xmpp.is_component: return self.window_counter = self.window self.enabled = False self.unacked_queue = collections.deque() register_stanza_plugin(StreamFeatures, stanza.StreamManagement) self.xmpp.register_stanza(stanza.Enable) self.xmpp.register_stanza(stanza.Enabled) self.xmpp.register_stanza(stanza.Resume) self.xmpp.register_stanza(stanza.Resumed) self.xmpp.register_stanza(stanza.Ack) self.xmpp.register_stanza(stanza.RequestAck) # Only end the session when a element is sent, # not just because the connection has died. self.xmpp.end_session_on_disconnect = False # Register the feature twice because it may be ordered two # different ways: enabling after binding and resumption # before binding. self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, order=self.order) self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, order=self.resume_order) self.xmpp.register_handler( Callback('Stream Management Enabled', MatchXPath(stanza.Enabled.tag_name()), self._handle_enabled, instream=True)) self.xmpp.register_handler( Callback('Stream Management Resumed', MatchXPath(stanza.Resumed.tag_name()), self._handle_resumed, instream=True)) self.xmpp.register_handler( Callback('Stream Management Failed', MatchXPath(stanza.Failed.tag_name()), self._handle_failed, instream=True)) self.xmpp.register_handler( Callback('Stream Management Ack', MatchXPath(stanza.Ack.tag_name()), self._handle_ack, instream=True)) self.xmpp.register_handler( Callback('Stream Management Request Ack', MatchXPath(stanza.RequestAck.tag_name()), self._handle_request_ack, instream=True)) self.xmpp.add_filter('in', self._handle_incoming) self.xmpp.add_filter('out_sync', self._handle_outgoing) self.xmpp.add_event_handler('session_end', self.session_end) def plugin_end(self): if self.xmpp.is_component: return self.xmpp.unregister_feature('sm', self.order) self.xmpp.unregister_feature('sm', self.resume_order) self.xmpp.del_event_handler('session_end', self.session_end) self.xmpp.del_filter('in', self._handle_incoming) self.xmpp.del_filter('out_sync', self._handle_outgoing) self.xmpp.remove_handler('Stream Management Enabled') self.xmpp.remove_handler('Stream Management Resumed') self.xmpp.remove_handler('Stream Management Failed') self.xmpp.remove_handler('Stream Management Ack') self.xmpp.remove_handler('Stream Management Request Ack') self.xmpp.remove_stanza(stanza.Enable) self.xmpp.remove_stanza(stanza.Enabled) self.xmpp.remove_stanza(stanza.Resume) self.xmpp.remove_stanza(stanza.Resumed) self.xmpp.remove_stanza(stanza.Ack) self.xmpp.remove_stanza(stanza.RequestAck) def session_end(self, event): """Reset stream management state.""" self.enabled = False self.unacked_queue.clear() self.sm_id = None self.handled = 0 self.seq = 0 self.last_ack = 0 def send_ack(self): """Send the current ack count to the server.""" ack = stanza.Ack(self.xmpp) ack['h'] = self.handled self.xmpp.send_raw(str(ack)) def request_ack(self, e=None): """Request an ack from the server.""" req = stanza.RequestAck(self.xmpp) self.xmpp.send_raw(str(req)) @asyncio.coroutine def _handle_sm_feature(self, features): """ Enable or resume stream management. If no SM-ID is stored, and resource binding has taken place, stream management will be enabled. If an SM-ID is known, and the server allows resumption, the previous stream will be resumed. """ if 'stream_management' in self.xmpp.features: # We've already negotiated stream management, # so no need to do it again. return False if not self.sm_id: if 'bind' in self.xmpp.features: enable = stanza.Enable(self.xmpp) enable['resume'] = self.allow_resume enable.send() self.enabled = True self.handled = 0 self.unacked_queue.clear() waiter = Waiter('enabled_or_failed', MatchMany([ MatchXPath(stanza.Enabled.tag_name()), MatchXPath(stanza.Failed.tag_name())])) self.xmpp.register_handler(waiter) result = yield from waiter.wait() elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features: self.enabled = True resume = stanza.Resume(self.xmpp) resume['h'] = self.handled resume['previd'] = self.sm_id resume.send() # Wait for a response before allowing stream feature processing # to continue. The actual result processing will be done in the # _handle_resumed() or _handle_failed() methods. waiter = Waiter('resumed_or_failed', MatchMany([ MatchXPath(stanza.Resumed.tag_name()), MatchXPath(stanza.Failed.tag_name())])) self.xmpp.register_handler(waiter) result = yield from waiter.wait() if result is not None and result.name == 'resumed': return True return False def _handle_enabled(self, stanza): """Save the SM-ID, if provided. Raises an :term:`sm_enabled` event. """ self.xmpp.features.add('stream_management') if stanza['id']: self.sm_id = stanza['id'] self.xmpp.event('sm_enabled', stanza) def _handle_resumed(self, stanza): """Finish resuming a stream by resending unacked stanzas. Raises a :term:`session_resumed` event. """ self.xmpp.features.add('stream_management') self._handle_ack(stanza) for id, stanza in self.unacked_queue: self.xmpp.send(stanza, use_filters=False) self.xmpp.event('session_resumed', stanza) def _handle_failed(self, stanza): """ Disable and reset any features used since stream management was requested (tracked stanzas may have been sent during the interval between the enable request and the enabled response). Raises an :term:`sm_failed` event. """ self.enabled = False self.unacked_queue.clear() self.xmpp.event('sm_failed', stanza) def _handle_ack(self, ack): """Process a server ack by freeing acked stanzas from the queue. Raises a :term:`stanza_acked` event for each acked stanza. """ if ack['h'] == self.last_ack: return num_acked = (ack['h'] - self.last_ack) % MAX_SEQ num_unacked = len(self.unacked_queue) log.debug("Ack: %s, Last Ack: %s, " + \ "Unacked: %s, Num Acked: %s, " + \ "Remaining: %s", ack['h'], self.last_ack, num_unacked, num_acked, num_unacked - num_acked) if num_acked > len(self.unacked_queue) or num_acked < 0: log.error('Inconsistent sequence numbers from the server,' ' ignoring and replacing ours with them.') num_acked = len(self.unacked_queue) for x in range(num_acked): seq, stanza = self.unacked_queue.popleft() self.xmpp.event('stanza_acked', stanza) self.last_ack = ack['h'] def _handle_request_ack(self, req): """Handle an ack request by sending an ack.""" self.send_ack() def _handle_incoming(self, stanza): """Increment the handled counter for each inbound stanza.""" if not self.enabled: return stanza if isinstance(stanza, (Message, Presence, Iq)): # Sequence numbers are mod 2^32 self.handled = (self.handled + 1) % MAX_SEQ return stanza def _handle_outgoing(self, stanza): """Store outgoing stanzas in a queue to be acked.""" if not self.enabled: return stanza if isinstance(stanza, (Message, Presence, Iq)): seq = None # Sequence numbers are mod 2^32 self.seq = (self.seq + 1) % MAX_SEQ seq = self.seq self.unacked_queue.append((seq, stanza)) self.window_counter -= 1 if self.window_counter == 0: self.window_counter = self.window self.request_ack() return stanza slixmpp-1.2.2/slixmpp/plugins/xep_0198/stanza.py0000644000175000001440000000672613004224717022513 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, StanzaBase class Enable(StanzaBase): name = 'enable' namespace = 'urn:xmpp:sm:3' interfaces = {'max', 'resume'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_resume(self): return self._get_attr('resume', 'false').lower() in ('true', '1') def set_resume(self, val): self._del_attr('resume') self._set_attr('resume', 'true' if val else 'false') class Enabled(StanzaBase): name = 'enabled' namespace = 'urn:xmpp:sm:3' interfaces = {'id', 'location', 'max', 'resume'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_resume(self): return self._get_attr('resume', 'false').lower() in ('true', '1') def set_resume(self, val): self._del_attr('resume') self._set_attr('resume', 'true' if val else 'false') class Resume(StanzaBase): name = 'resume' namespace = 'urn:xmpp:sm:3' interfaces = {'h', 'previd'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) class Resumed(StanzaBase): name = 'resumed' namespace = 'urn:xmpp:sm:3' interfaces = {'h', 'previd'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) class Failed(StanzaBase, Error): name = 'failed' namespace = 'urn:xmpp:sm:3' interfaces = set() def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class StreamManagement(ElementBase): name = 'sm' namespace = 'urn:xmpp:sm:3' plugin_attrib = name interfaces = {'required', 'optional'} def get_required(self): return self.xml.find('{%s}required' % self.namespace) is not None def set_required(self, val): self.del_required() if val: self._set_sub_text('required', '', keep=True) def del_required(self): self._del_sub('required') def get_optional(self): return self.xml.find('{%s}optional' % self.namespace) is not None def set_optional(self, val): self.del_optional() if val: self._set_sub_text('optional', '', keep=True) def del_optional(self): self._del_sub('optional') class RequestAck(StanzaBase): name = 'r' namespace = 'urn:xmpp:sm:3' interfaces = set() def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class Ack(StanzaBase): name = 'a' namespace = 'urn:xmpp:sm:3' interfaces = {'h'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) slixmpp-1.2.2/slixmpp/plugins/xep_0270.py0000644000175000001440000000076013004224717021172 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins import BasePlugin, register_plugin class XEP_0270(BasePlugin): name = 'xep_0270' description = 'XEP-0270: XMPP Compliance Suites 2010' dependencies = {'xep_0030', 'xep_0115', 'xep_0054', 'xep_0163', 'xep_0045', 'xep_0085'} register_plugin(XEP_0270) slixmpp-1.2.2/slixmpp/plugins/xep_0231/0000755000175000001440000000000013014656513020616 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0231/bob.py0000644000175000001440000001033713004224717021732 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import hashlib from slixmpp import future_wrapper from slixmpp.stanza import Iq, Message, Presence from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0231 import stanza, BitsOfBinary log = logging.getLogger(__name__) class XEP_0231(BasePlugin): """ XEP-0231 Bits of Binary """ name = 'xep_0231' description = 'XEP-0231: Bits of Binary' dependencies = {'xep_0030'} def plugin_init(self): self._cids = {} register_stanza_plugin(Iq, BitsOfBinary) register_stanza_plugin(Message, BitsOfBinary) register_stanza_plugin(Presence, BitsOfBinary) self.xmpp.register_handler( Callback('Bits of Binary - Iq', StanzaPath('iq/bob'), self._handle_bob_iq)) self.xmpp.register_handler( Callback('Bits of Binary - Message', StanzaPath('message/bob'), self._handle_bob)) self.xmpp.register_handler( Callback('Bits of Binary - Presence', StanzaPath('presence/bob'), self._handle_bob)) self.api.register(self._get_bob, 'get_bob', default=True) self.api.register(self._set_bob, 'set_bob', default=True) self.api.register(self._del_bob, 'del_bob', default=True) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:bob') self.xmpp.remove_handler('Bits of Binary - Iq') self.xmpp.remove_handler('Bits of Binary - Message') self.xmpp.remove_handler('Bits of Binary - Presence') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:bob') def set_bob(self, data, mtype, cid=None, max_age=None): if cid is None: cid = 'sha1+%s@bob.xmpp.org' % hashlib.sha1(data).hexdigest() bob = BitsOfBinary() bob['data'] = data bob['type'] = mtype bob['cid'] = cid bob['max_age'] = max_age self.api['set_bob'](args=bob) return cid @future_wrapper def get_bob(self, jid=None, cid=None, cached=True, ifrom=None, timeout=None, callback=None): if cached: data = self.api['get_bob'](None, None, ifrom, args=cid) if data is not None: if not isinstance(data, Iq): iq = self.xmpp.Iq() iq.append(data) return iq return data iq = self.xmpp.Iq() iq['to'] = jid iq['from'] = ifrom iq['type'] = 'get' iq['bob']['cid'] = cid return iq.send(timeout=timeout, callback=callback) def del_bob(self, cid): self.api['del_bob'](args=cid) def _handle_bob_iq(self, iq): cid = iq['bob']['cid'] if iq['type'] == 'result': self.api['set_bob'](iq['from'], None, iq['to'], args=iq['bob']) self.xmpp.event('bob', iq) elif iq['type'] == 'get': data = self.api['get_bob'](iq['to'], None, iq['from'], args=cid) if isinstance(data, Iq): data['id'] = iq['id'] data.send() return iq = iq.reply() iq.append(data) iq.send() def _handle_bob(self, stanza): self.api['set_bob'](stanza['from'], None, stanza['to'], args=stanza['bob']) self.xmpp.event('bob', stanza) # ================================================================= def _set_bob(self, jid, node, ifrom, bob): self._cids[bob['cid']] = bob def _get_bob(self, jid, node, ifrom, cid): if cid in self._cids: return self._cids[cid] raise XMPPError('item-not-found') def _del_bob(self, jid, node, ifrom, cid): if cid in self._cids: del self._cids[cid] slixmpp-1.2.2/slixmpp/plugins/xep_0231/__init__.py0000644000175000001440000000065112424504520022723 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0231.stanza import BitsOfBinary from slixmpp.plugins.xep_0231.bob import XEP_0231 register_plugin(XEP_0231) slixmpp-1.2.2/slixmpp/plugins/xep_0231/stanza.py0000644000175000001440000000156013004224717022466 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ import base64 from slixmpp.util import bytes from slixmpp.xmlstream import ElementBase class BitsOfBinary(ElementBase): name = 'data' namespace = 'urn:xmpp:bob' plugin_attrib = 'bob' interfaces = {'cid', 'max_age', 'type', 'data'} def get_max_age(self): return int(self._get_attr('max-age')) def set_max_age(self, value): self._set_attr('max-age', str(value)) def get_data(self): return base64.b64decode(bytes(self.xml.text)) def set_data(self, value): self.xml.text = bytes(base64.b64encode(value)).decode('utf-8') def del_data(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/plugins/gmail_notify.py0000644000175000001440000001107213004224717022405 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.plugins import BasePlugin from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream.stanzabase import register_stanza_plugin, ElementBase, ET, JID from .. stanza.iq import Iq log = logging.getLogger(__name__) class GmailQuery(ElementBase): namespace = 'google:mail:notify' name = 'query' plugin_attrib = 'gmail' interfaces = {'newer-than-time', 'newer-than-tid', 'q', 'search'} def get_search(self): return self['q'] def set_search(self, search): self['q'] = search def del_search(self): del self['q'] class MailBox(ElementBase): namespace = 'google:mail:notify' name = 'mailbox' plugin_attrib = 'mailbox' interfaces = {'result-time', 'total-matched', 'total-estimate', 'url', 'threads', 'matched', 'estimate'} def get_threads(self): threads = [] for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace, MailThread.name)): threads.append(MailThread(xml=threadXML, parent=None)) return threads def get_matched(self): return self['total-matched'] def get_estimate(self): return self['total-estimate'] == '1' class MailThread(ElementBase): namespace = 'google:mail:notify' name = 'mail-thread-info' plugin_attrib = 'thread' interfaces = {'tid', 'participation', 'messages', 'date', 'senders', 'url', 'labels', 'subject', 'snippet'} sub_interfaces = {'labels', 'subject', 'snippet'} def get_senders(self): senders = [] sendersXML = self.xml.find('{%s}senders' % self.namespace) if sendersXML is not None: for senderXML in sendersXML.findall('{%s}sender' % self.namespace): senders.append(MailSender(xml=senderXML, parent=None)) return senders class MailSender(ElementBase): namespace = 'google:mail:notify' name = 'sender' plugin_attrib = 'sender' interfaces = {'address', 'name', 'originator', 'unread'} def get_originator(self): return self.xml.attrib.get('originator', '0') == '1' def get_unread(self): return self.xml.attrib.get('unread', '0') == '1' class NewMail(ElementBase): namespace = 'google:mail:notify' name = 'new-mail' plugin_attrib = 'new-mail' class gmail_notify(BasePlugin): """ Google Talk: Gmail Notifications """ def plugin_init(self): self.description = 'Google Talk: Gmail Notifications' self.xmpp.registerHandler( Callback('Gmail Result', MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, MailBox.namespace, MailBox.name)), self.handle_gmail)) self.xmpp.registerHandler( Callback('Gmail New Mail', MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, NewMail.namespace, NewMail.name)), self.handle_new_mail)) register_stanza_plugin(Iq, GmailQuery) register_stanza_plugin(Iq, MailBox) register_stanza_plugin(Iq, NewMail) self.last_result_time = None def handle_gmail(self, iq): mailbox = iq['mailbox'] approx = ' approximately' if mailbox['estimated'] else '' log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched']) self.last_result_time = mailbox['result-time'] self.xmpp.event('gmail_messages', iq) def handle_new_mail(self, iq): log.info("Gmail: New emails received!") self.xmpp.event('gmail_notify') self.checkEmail() def getEmail(self, query=None): return self.search(query) def checkEmail(self): return self.search(newer=self.last_result_time) def search(self, query=None, newer=None): if query is None: log.info("Gmail: Checking for new emails") else: log.info('Gmail: Searching for emails matching: "%s"', query) iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = self.xmpp.boundjid.bare iq['gmail']['q'] = query iq['gmail']['newer-than-time'] = newer return iq.send() slixmpp-1.2.2/slixmpp/plugins/xep_0128/0000755000175000001440000000000013014656513020623 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0128/__init__.py0000644000175000001440000000061012770302340022723 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0128.static import StaticExtendedDisco from slixmpp.plugins.xep_0128.extended_disco import XEP_0128 register_plugin(XEP_0128) slixmpp-1.2.2/slixmpp/plugins/xep_0128/static.py0000644000175000001440000000370412774761476022511 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) class StaticExtendedDisco(object): """ Extend the default StaticDisco implementation to provide support for extended identity information. """ def __init__(self, static): """ Augment the default XEP-0030 static handler object. Arguments: static -- The default static XEP-0030 handler object. """ self.static = static def set_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.del_extended_info(jid, node, ifrom, data) self.add_extended_info(jid, node, ifrom, data) def add_extended_info(self, jid, node, ifrom, data): """ Add additional extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.static.add_node(jid, node) forms = data.get('data', []) if not isinstance(forms, list): forms = [forms] info = self.static.get_node(jid, node)['info'] for form in forms: info.append(form) def del_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter is not used. """ if self.static.node_exists(jid, node): info = self.static.get_node(jid, node)['info'] for form in info['substanza']: info.xml.remove(form.xml) slixmpp-1.2.2/slixmpp/plugins/xep_0128/extended_disco.py0000644000175000001440000000624613004224717024162 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import slixmpp from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0030 import DiscoInfo from slixmpp.plugins.xep_0128 import StaticExtendedDisco class XEP_0128(BasePlugin): """ XEP-0128: Service Discovery Extensions Allow the use of data forms to add additional identity information to disco#info results. Also see . Attributes: disco -- A reference to the XEP-0030 plugin. static -- Object containing the default set of static node handlers. xmpp -- The main Slixmpp object. Methods: set_extended_info -- Set extensions to a disco#info result. add_extended_info -- Add an extension to a disco#info result. del_extended_info -- Remove all extensions from a disco#info result. """ name = 'xep_0128' description = 'XEP-0128: Service Discovery Extensions' dependencies = {'xep_0030', 'xep_0004'} def plugin_init(self): """Start the XEP-0128 plugin.""" self._disco_ops = ['set_extended_info', 'add_extended_info', 'del_extended_info'] register_stanza_plugin(DiscoInfo, Form, iterable=True) self.disco = self.xmpp['xep_0030'] self.static = StaticExtendedDisco(self.disco.static) self.disco.set_extended_info = self.set_extended_info self.disco.add_extended_info = self.add_extended_info self.disco.del_extended_info = self.del_extended_info for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) def set_extended_info(self, jid=None, node=None, **kwargs): """ Set additional, extended identity information to a node. Replaces any existing extended information. Arguments: jid -- The JID to modify. node -- The node to modify. data -- Either a form, or a list of forms to use as extended information, replacing any existing extensions. """ self.api['set_extended_info'](jid, node, None, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs): """ Add additional, extended identity information to a node. Arguments: jid -- The JID to modify. node -- The node to modify. data -- Either a form, or a list of forms to add as extended information. """ self.api['add_extended_info'](jid, node, None, kwargs) def del_extended_info(self, jid=None, node=None, **kwargs): """ Remove all extended identity information to a node. Arguments: jid -- The JID to modify. node -- The node to modify. """ self.api['del_extended_info'](jid, node, None, kwargs) slixmpp-1.2.2/slixmpp/plugins/base.py0000644000175000001440000002731112773536011020646 0ustar mathieuiusers00000000000000# -*- encoding: utf-8 -*- """ slixmpp.plugins.base ~~~~~~~~~~~~~~~~~~~~~~ This module provides XMPP functionality that is specific to client connections. Part of Slixmpp: The Slick XMPP Library :copyright: (c) 2012 Nathanael C. Fritz :license: MIT, see LICENSE for more details """ import sys import copy import logging import threading log = logging.getLogger(__name__) #: Associate short string names of plugins with implementations. The #: plugin names are based on the spec used by the plugin, such as #: `'xep_0030'` for a plugin that implements XEP-0030. PLUGIN_REGISTRY = {} #: In order to do cascading plugin disabling, reverse dependencies #: must be tracked. PLUGIN_DEPENDENTS = {} #: Only allow one thread to manipulate the plugin registry at a time. REGISTRY_LOCK = threading.RLock() class PluginNotFound(Exception): """Raised if an unknown plugin is accessed.""" def register_plugin(impl, name=None): """Add a new plugin implementation to the registry. :param class impl: The plugin class. The implementation class must provide a :attr:`~BasePlugin.name` value that will be used as a short name for enabling and disabling the plugin. The name should be based on the specification used by the plugin. For example, a plugin implementing XEP-0030 would be named `'xep_0030'`. """ if name is None: name = impl.name with REGISTRY_LOCK: PLUGIN_REGISTRY[name] = impl if name not in PLUGIN_DEPENDENTS: PLUGIN_DEPENDENTS[name] = set() for dep in impl.dependencies: if dep not in PLUGIN_DEPENDENTS: PLUGIN_DEPENDENTS[dep] = set() PLUGIN_DEPENDENTS[dep].add(name) def load_plugin(name, module=None): """Find and import a plugin module so that it can be registered. This function is called to import plugins that have selected for enabling, but no matching registered plugin has been found. :param str name: The name of the plugin. It is expected that plugins are in packages matching their name, even though the plugin class name does not have to match. :param str module: The name of the base module to search for the plugin. """ try: if not module: try: module = 'slixmpp.plugins.%s' % name __import__(module) mod = sys.modules[module] except ImportError: module = 'slixmpp.features.%s' % name __import__(module) mod = sys.modules[module] elif isinstance(module, str): __import__(module) mod = sys.modules[module] else: mod = module # Add older style plugins to the registry. if hasattr(mod, name): plugin = getattr(mod, name) if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'): plugin.name = name # Mark the plugin as an older style plugin so # we can work around dependency issues. plugin.old_style = True register_plugin(plugin, name) except ImportError: log.exception("Unable to load plugin: %s", name) class PluginManager(object): def __init__(self, xmpp, config=None): #: We will track all enabled plugins in a set so that we #: can enable plugins in batches and pull in dependencies #: without problems. self._enabled = set() #: Maintain references to active plugins. self._plugins = {} self._plugin_lock = threading.RLock() #: Globally set default plugin configuration. This will #: be used for plugins that are auto-enabled through #: dependency loading. self.config = config if config else {} self.xmpp = xmpp def register(self, plugin, enable=True): """Register a new plugin, and optionally enable it. :param class plugin: The implementation class of the plugin to register. :param bool enable: If ``True``, immediately enable the plugin after registration. """ register_plugin(plugin) if enable: self.enable(plugin.name) def enable(self, name, config=None, enabled=None): """Enable a plugin, including any dependencies. :param string name: The short name of the plugin. :param dict config: Optional settings dictionary for configuring plugin behaviour. """ if enabled is None: enabled = set() with self._plugin_lock: if name not in self._enabled: enabled.add(name) self._enabled.add(name) if not self.registered(name): load_plugin(name) plugin_class = PLUGIN_REGISTRY.get(name, None) if not plugin_class: raise PluginNotFound(name) if config is None: config = self.config.get(name, None) plugin = plugin_class(self.xmpp, config) self._plugins[name] = plugin for dep in plugin.dependencies: self.enable(dep, enabled=enabled) plugin._init() for name in enabled: if hasattr(self._plugins[name], 'old_style'): # Older style plugins require post_init() # to run just before stream processing begins, # so we don't call it here. pass else: self._plugins[name].post_init() def enable_all(self, names=None, config=None): """Enable all registered plugins. :param list names: A list of plugin names to enable. If none are provided, all registered plugins will be enabled. :param dict config: A dictionary mapping plugin names to configuration dictionaries, as used by :meth:`~PluginManager.enable`. """ names = names if names else PLUGIN_REGISTRY.keys() if config is None: config = {} for name in names: self.enable(name, config.get(name, {})) def enabled(self, name): """Check if a plugin has been enabled. :param string name: The name of the plugin to check. :return: boolean """ return name in self._enabled def registered(self, name): """Check if a plugin has been registered. :param string name: The name of the plugin to check. :return: boolean """ return name in PLUGIN_REGISTRY def disable(self, name, _disabled=None): """Disable a plugin, including any dependent upon it. :param string name: The name of the plugin to disable. :param set _disabled: Private set used to track the disabled status of plugins during the cascading process. """ if _disabled is None: _disabled = set() with self._plugin_lock: if name not in _disabled and name in self._enabled: _disabled.add(name) plugin = self._plugins.get(name, None) if plugin is None: raise PluginNotFound(name) for dep in PLUGIN_DEPENDENTS[name]: self.disable(dep, _disabled) plugin._end() if name in self._enabled: self._enabled.remove(name) del self._plugins[name] def __keys__(self): """Return the set of enabled plugins.""" return self._plugins.keys() def __getitem__(self, name): """ Allow plugins to be accessed through the manager as if it were a dictionary. """ plugin = self._plugins.get(name, None) if plugin is None: raise PluginNotFound(name) return plugin def __iter__(self): """Return an iterator over the set of enabled plugins.""" return self._plugins.__iter__() def __len__(self): """Return the number of enabled plugins.""" return len(self._plugins) class BasePlugin(object): #: A short name for the plugin based on the implemented specification. #: For example, a plugin for XEP-0030 would use `'xep_0030'`. name = '' #: A longer name for the plugin, describing its purpose. For example, #: a plugin for XEP-0030 would use `'Service Discovery'` as its #: description value. description = '' #: Some plugins may depend on others in order to function properly. #: Any plugin names included in :attr:`~BasePlugin.dependencies` will #: be initialized as needed if this plugin is enabled. dependencies = set() #: The basic, standard configuration for the plugin, which may #: be overridden when initializing the plugin. The configuration #: fields included here may be accessed directly as attributes of #: the plugin. For example, including the configuration field 'foo' #: would mean accessing `plugin.foo` returns the current value of #: `plugin.config['foo']`. default_config = {} def __init__(self, xmpp, config=None): self.xmpp = xmpp if self.xmpp: self.api = self.xmpp.api.wrap(self.name) #: A plugin's behaviour may be configurable, in which case those #: configuration settings will be provided as a dictionary. self.config = copy.copy(self.default_config) if config: self.config.update(config) def __getattr__(self, key): """Provide direct access to configuration fields. If the standard configuration includes the option `'foo'`, then accessing `self.foo` should be the same as `self.config['foo']`. """ if key in self.default_config: return self.config.get(key, None) else: return object.__getattribute__(self, key) def __setattr__(self, key, value): """Provide direct assignment to configuration fields. If the standard configuration includes the option `'foo'`, then assigning to `self.foo` should be the same as assigning to `self.config['foo']`. """ if key in self.default_config: self.config[key] = value else: super().__setattr__(key, value) def _init(self): """Initialize plugin state, such as registering event handlers. Also sets up required event handlers. """ if self.xmpp is not None: self.xmpp.add_event_handler('session_bind', self.session_bind) if self.xmpp.session_bind_event.is_set(): self.session_bind(self.xmpp.boundjid.full) self.plugin_init() log.debug('Loaded Plugin: %s', self.description) def _end(self): """Cleanup plugin state, and prepare for plugin removal. Also removes required event handlers. """ if self.xmpp is not None: self.xmpp.del_event_handler('session_bind', self.session_bind) self.plugin_end() log.debug('Disabled Plugin: %s' % self.description) def plugin_init(self): """Initialize plugin state, such as registering event handlers.""" pass def plugin_end(self): """Cleanup plugin state, and prepare for plugin removal.""" pass def session_bind(self, jid): """Initialize plugin state based on the bound JID.""" pass def post_init(self): """Initialize any cross-plugin state. Only needed if the plugin has circular dependencies. """ pass slixmpp-1.2.2/slixmpp/plugins/xep_0223.py0000644000175000001440000001137413004226004021162 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin, register_plugin log = logging.getLogger(__name__) class XEP_0223(BasePlugin): """ XEP-0223: Persistent Storage of Private Data via PubSub """ name = 'xep_0223' description = 'XEP-0223: Persistent Storage of Private Data via PubSub' dependencies = {'xep_0163', 'xep_0060', 'xep_0004'} profile = {'pubsub#persist_items': True, 'pubsub#send_last_published_item': 'never'} def configure(self, node, ifrom=None, callback=None, timeout=None): """ Update a node's configuration to match the public storage profile. """ # TODO: that cannot possibly work, why is this here? config = self.xmpp['xep_0004'].Form() config['type'] = 'submit' for field, value in self.profile.items(): config.add_field(var=field, value=value) return self.xmpp['xep_0060'].set_node_config(None, node, config, ifrom=ifrom, callback=callback, timeout=timeout) def store(self, stanza, node=None, id=None, ifrom=None, options=None, callback=None, timeout=None, timeout_callback=None): """ Store private data via PEP. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. Arguments: stanza -- The private content to store. node -- The node to publish the content to. If not specified, the stanza's namespace will be used. id -- Optionally specify the ID of the item. options -- Publish options to use, which will be modified to fit the persistent storage option profile. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if not options: options = self.xmpp['xep_0004'].stanza.Form() options['type'] = 'submit' options.add_field( var='FORM_TYPE', ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') fields = options['fields'] for field, value in self.profile.items(): if field not in fields: options.add_field(var=field) options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def retrieve(self, node, id=None, item_ids=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Retrieve private data via PEP. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. Arguments: node -- The node to retrieve content from. id -- Optionally specify the ID of the item. item_ids -- Specify a group of IDs. If id is also specified, it will be included in item_ids. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if item_ids is None: item_ids = [] if id is not None: item_ids.append(id) return self.xmpp['xep_0060'].get_items(None, node, item_ids=item_ids, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) register_plugin(XEP_0223) slixmpp-1.2.2/slixmpp/plugins/xep_0279/0000755000175000001440000000000013014656513020632 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0279/__init__.py0000644000175000001440000000064112424504520022736 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0279 import stanza from slixmpp.plugins.xep_0279.stanza import IPCheck from slixmpp.plugins.xep_0279.ipcheck import XEP_0279 register_plugin(XEP_0279) slixmpp-1.2.2/slixmpp/plugins/xep_0279/stanza.py0000644000175000001440000000120313004224717022474 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class IPCheck(ElementBase): name = 'ip' namespace = 'urn:xmpp:sic:0' plugin_attrib = 'ip_check' interfaces = {'ip_check'} is_extension = True def get_ip_check(self): return self.xml.text def set_ip_check(self, value): if value: self.xml.text = value else: self.xml.text = '' def del_ip_check(self): self.xml.text = '' slixmpp-1.2.2/slixmpp/plugins/xep_0279/ipcheck.py0000644000175000001440000000217713004224717022615 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0279 import stanza, IPCheck class XEP_0279(BasePlugin): name = 'xep_0279' description = 'XEP-0279: Server IP Check' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, IPCheck) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:sic:0') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0') def check_ip(self, ifrom=None, block=True, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['from'] = ifrom iq.enable('ip_check') return iq.send(block=block, timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0257/0000755000175000001440000000000013014656513020626 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0257/__init__.py0000644000175000001440000000077612424504520022743 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0257 import stanza from slixmpp.plugins.xep_0257.stanza import Certs, AppendCert from slixmpp.plugins.xep_0257.stanza import DisableCert, RevokeCert from slixmpp.plugins.xep_0257.client_cert_management import XEP_0257 register_plugin(XEP_0257) slixmpp-1.2.2/slixmpp/plugins/xep_0257/client_cert_management.py0000644000175000001440000000466413004224717025675 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0257 import stanza, Certs from slixmpp.plugins.xep_0257 import AppendCert, DisableCert, RevokeCert log = logging.getLogger(__name__) class XEP_0257(BasePlugin): name = 'xep_0257' description = 'XEP-0257: Client Certificate Management for SASL EXTERNAL' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, Certs) register_stanza_plugin(Iq, AppendCert) register_stanza_plugin(Iq, DisableCert) register_stanza_plugin(Iq, RevokeCert) def get_certs(self, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['from'] = ifrom iq.enable('sasl_certs') return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def add_cert(self, name, cert, allow_management=True, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['sasl_cert_append']['name'] = name iq['sasl_cert_append']['x509cert'] = cert iq['sasl_cert_append']['cert_management'] = allow_management return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def disable_cert(self, name, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['sasl_cert_disable']['name'] = name return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def revoke_cert(self, name, ifrom=None, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['sasl_cert_revoke']['name'] = name return iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0257/stanza.py0000644000175000001440000000475113004224717022503 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class Certs(ElementBase): name = 'items' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'sasl_certs' interfaces = set() class CertItem(ElementBase): name = 'item' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'item' plugin_multi_attrib = 'items' interfaces = {'name', 'x509cert', 'users'} sub_interfaces = {'name', 'x509cert'} def get_users(self): resources = self.xml.findall('{%s}users/{%s}resource' % ( self.namespace, self.namespace)) return {res.text for res in resources} def set_users(self, values): users = self.xml.find('{%s}users' % self.namespace) if users is None: users = ET.Element('{%s}users' % self.namespace) self.xml.append(users) for resource in values: res = ET.Element('{%s}resource' % self.namespace) res.text = resource users.append(res) def del_users(self): users = self.xml.find('{%s}users' % self.namespace) if users is not None: self.xml.remove(users) class AppendCert(ElementBase): name = 'append' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'sasl_cert_append' interfaces = {'name', 'x509cert', 'cert_management'} sub_interfaces = {'name', 'x509cert'} def get_cert_management(self): manage = self.xml.find('{%s}no-cert-management' % self.namespace) return manage is None def set_cert_management(self, value): self.del_cert_management() if not value: manage = ET.Element('{%s}no-cert-management' % self.namespace) self.xml.append(manage) def del_cert_management(self): manage = self.xml.find('{%s}no-cert-management' % self.namespace) if manage is not None: self.xml.remove(manage) class DisableCert(ElementBase): name = 'disable' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'sasl_cert_disable' interfaces = {'name'} sub_interfaces = interfaces class RevokeCert(ElementBase): name = 'revoke' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'sasl_cert_revoke' interfaces = {'name'} sub_interfaces = interfaces register_stanza_plugin(Certs, CertItem, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0115/0000755000175000001440000000000013014656513020617 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0115/__init__.py0000644000175000001440000000065612770302340022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0115.stanza import Capabilities from slixmpp.plugins.xep_0115.static import StaticCaps from slixmpp.plugins.xep_0115.caps import XEP_0115 register_plugin(XEP_0115) slixmpp-1.2.2/slixmpp/plugins/xep_0115/caps.py0000644000175000001440000002752713004224717022130 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import hashlib import base64 from slixmpp import __version__ from slixmpp.stanza import StreamFeatures, Presence, Iq from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp import asyncio from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0115 import stanza, StaticCaps log = logging.getLogger(__name__) class XEP_0115(BasePlugin): """ XEP-0115: Entity Capabalities """ name = 'xep_0115' description = 'XEP-0115: Entity Capabilities' dependencies = {'xep_0030', 'xep_0128', 'xep_0004'} stanza = stanza default_config = { 'hash': 'sha-1', 'caps_node': None, 'broadcast': True } def plugin_init(self): self.hashes = {'sha-1': hashlib.sha1, 'sha1': hashlib.sha1, 'md5': hashlib.md5} if self.caps_node is None: self.caps_node = 'http://slixmpp.com/ver/%s' % __version__ register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities) self._disco_ops = ['cache_caps', 'get_caps', 'assign_verstring', 'get_verstring', 'supports', 'has_identity'] self.xmpp.register_handler( Callback('Entity Capabilites', StanzaPath('presence/caps'), self._handle_caps)) self.xmpp.add_filter('out', self._filter_add_caps) self.xmpp.add_event_handler('entity_caps', self._process_caps) if not self.xmpp.is_component: self.xmpp.register_feature('caps', self._handle_caps_feature, restart=False, order=10010) disco = self.xmpp['xep_0030'] self.static = StaticCaps(self.xmpp, disco.static) for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) for op in ('supports', 'has_identity'): self.xmpp['xep_0030'].api.register(getattr(self.static, op), op) self._run_node_handler = disco._run_node_handler disco.cache_caps = self.cache_caps disco.update_caps = self.update_caps disco.assign_verstring = self.assign_verstring disco.get_verstring = self.get_verstring def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) self.xmpp.del_filter('out', self._filter_add_caps) self.xmpp.del_event_handler('entity_caps', self._process_caps) self.xmpp.remove_handler('Entity Capabilities') if not self.xmpp.is_component: self.xmpp.unregister_feature('caps', 10010) for op in ('supports', 'has_identity'): self.xmpp['xep_0030'].restore_defaults(op) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) def _filter_add_caps(self, stanza): if not isinstance(stanza, Presence) or not self.broadcast: return stanza if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'): return stanza ver = self.get_verstring(stanza['from']) if ver: stanza['caps']['node'] = self.caps_node stanza['caps']['hash'] = self.hash stanza['caps']['ver'] = ver return stanza def _handle_caps(self, presence): if not self.xmpp.is_component: if presence['from'] == self.xmpp.boundjid: return self.xmpp.event('entity_caps', presence) def _handle_caps_feature(self, features): # We already have a method to process presence with # caps, so wrap things up and use that. p = Presence() p['from'] = self.xmpp.boundjid.domain p.append(features['caps']) self.xmpp.features.add('caps') self.xmpp.event('entity_caps', p) @asyncio.coroutine def _process_caps(self, pres): if not pres['caps']['hash']: log.debug("Received unsupported legacy caps: %s, %s, %s", pres['caps']['node'], pres['caps']['ver'], pres['caps']['ext']) self.xmpp.event('entity_caps_legacy', pres) return ver = pres['caps']['ver'] existing_verstring = self.get_verstring(pres['from'].full) if str(existing_verstring) == str(ver): return existing_caps = self.get_caps(verstring=ver) if existing_caps is not None: self.assign_verstring(pres['from'], ver) return if pres['caps']['hash'] not in self.hashes: try: log.debug("Unknown caps hash: %s", pres['caps']['hash']) self.xmpp['xep_0030'].get_info(jid=pres['from']) return except XMPPError: return log.debug("New caps verification string: %s", ver) try: node = '%s#%s' % (pres['caps']['node'], ver) caps = yield from self.xmpp['xep_0030'].get_info(pres['from'], node, coroutine=True) if isinstance(caps, Iq): caps = caps['disco_info'] if self._validate_caps(caps, pres['caps']['hash'], pres['caps']['ver']): self.assign_verstring(pres['from'], pres['caps']['ver']) except XMPPError: log.debug("Could not retrieve disco#info results for caps for %s", node) def _validate_caps(self, caps, hash, check_verstring): # Check Identities full_ids = caps.get_identities(dedupe=False) deduped_ids = caps.get_identities() if len(full_ids) != len(deduped_ids): log.debug("Duplicate disco identities found, invalid for caps") return False # Check Features full_features = caps.get_features(dedupe=False) deduped_features = caps.get_features() if len(full_features) != len(deduped_features): log.debug("Duplicate disco features found, invalid for caps") return False # Check Forms form_types = [] deduped_form_types = set() for stanza in caps['substanzas']: if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): log.debug("Non form extension found, ignoring for caps") caps.xml.remove(stanza.xml) continue if 'FORM_TYPE' in stanza['fields']: f_type = tuple(stanza['fields']['FORM_TYPE']['value']) form_types.append(f_type) deduped_form_types.add(f_type) if len(form_types) != len(deduped_form_types): log.debug("Duplicated FORM_TYPE values, " + \ "invalid for caps") return False if len(f_type) > 1: deduped_type = set(f_type) if len(f_type) != len(deduped_type): log.debug("Extra FORM_TYPE data, invalid for caps") return False if stanza['fields']['FORM_TYPE']['type'] != 'hidden': log.debug("Field FORM_TYPE type not 'hidden', " + \ "ignoring form for caps") caps.xml.remove(stanza.xml) else: log.debug("No FORM_TYPE found, ignoring form for caps") caps.xml.remove(stanza.xml) verstring = self.generate_verstring(caps, hash) if verstring != check_verstring: log.debug("Verification strings do not match: %s, %s" % ( verstring, check_verstring)) return False self.cache_caps(verstring, caps) return True def generate_verstring(self, info, hash): hash = self.hashes.get(hash, None) if hash is None: return None S = '' # Convert None to '' in the identities def clean_identity(id): return map(lambda i: i or '', id) identities = map(clean_identity, info['identities']) identities = sorted(('/'.join(i) for i in identities)) features = sorted(info['features']) S += '<'.join(identities) + '<' S += '<'.join(features) + '<' form_types = {} for stanza in info['substanzas']: if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): if 'FORM_TYPE' in stanza['fields']: f_type = stanza['values']['FORM_TYPE'] if len(f_type): f_type = f_type[0] if f_type not in form_types: form_types[f_type] = [] form_types[f_type].append(stanza) sorted_forms = sorted(form_types.keys()) for f_type in sorted_forms: for form in form_types[f_type]: S += '%s<' % f_type fields = sorted(form['fields'].keys()) fields.remove('FORM_TYPE') for field in fields: S += '%s<' % field vals = form['fields'][field].get_value(convert=False) if vals is None: S += '<' else: if not isinstance(vals, list): vals = [vals] S += '<'.join(sorted(vals)) + '<' binary = hash(S.encode('utf8')).digest() return base64.b64encode(binary).decode('utf-8') @asyncio.coroutine def update_caps(self, jid=None, node=None, preserve=False): try: info = yield from self.xmpp['xep_0030'].get_info(jid, node, local=True) if isinstance(info, Iq): info = info['disco_info'] ver = self.generate_verstring(info, self.hash) self.xmpp['xep_0030'].set_info( jid=jid, node='%s#%s' % (self.caps_node, ver), info=info) self.cache_caps(ver, info) self.assign_verstring(jid, ver) if self.xmpp.sessionstarted and self.broadcast: if self.xmpp.is_component or preserve: for contact in self.xmpp.roster[jid]: self.xmpp.roster[jid][contact].send_last_presence() else: self.xmpp.roster[jid].send_last_presence() except XMPPError: return def get_verstring(self, jid=None): if jid in ('', None): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full return self.api['get_verstring'](jid) def assign_verstring(self, jid=None, verstring=None): if jid in (None, ''): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full return self.api['assign_verstring'](jid, args={ 'verstring': verstring}) def cache_caps(self, verstring=None, info=None): data = {'verstring': verstring, 'info': info} return self.api['cache_caps'](args=data) def get_caps(self, jid=None, verstring=None): if verstring is None: if jid is not None: verstring = self.get_verstring(jid) else: return None if isinstance(jid, JID): jid = jid.full data = {'verstring': verstring} return self.api['get_caps'](jid, args=data) slixmpp-1.2.2/slixmpp/plugins/xep_0115/static.py0000644000175000001440000001211612770302340022453 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.xmlstream import JID from slixmpp.exceptions import IqError, IqTimeout log = logging.getLogger(__name__) class StaticCaps(object): """ Extend the default StaticDisco implementation to provide support for extended identity information. """ def __init__(self, xmpp, static): """ Augment the default XEP-0030 static handler object. Arguments: static -- The default static XEP-0030 handler object. """ self.xmpp = xmpp self.disco = self.xmpp['xep_0030'] self.caps = self.xmpp['xep_0115'] self.static = static self.ver_cache = {} self.jid_vers = {} def supports(self, jid, node, ifrom, data): """ Check if a JID supports a given feature. The data parameter may provide: feature -- The feature to check for support. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ feature = data.get('feature', None) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} if not feature: return False if node in (None, ''): info = self.caps.get_caps(jid) if info and feature in info['features']: return True try: info = self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) return feature in info['disco_info']['features'] except IqError: return False except IqTimeout: return None def has_identity(self, jid, node, ifrom, data): """ Check if a JID has a given identity. The data parameter may provide: category -- The category of the identity to check. itype -- The type of the identity to check. lang -- The language of the identity to check. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ identity = (data.get('category', None), data.get('itype', None), data.get('lang', None)) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} trunc = lambda i: (i[0], i[1], i[2]) if node in (None, ''): info = self.caps.get_caps(jid) if info and identity in map(trunc, info['identities']): return True try: info = self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) return identity in map(trunc, info['disco_info']['identities']) except IqError: return False except IqTimeout: return None def cache_caps(self, jid, node, ifrom, data): verstring = data.get('verstring', None) info = data.get('info', None) if not verstring or not info: return self.ver_cache[verstring] = info def assign_verstring(self, jid, node, ifrom, data): if isinstance(jid, JID): jid = jid.full self.jid_vers[jid] = data.get('verstring', None) def get_verstring(self, jid, node, ifrom, data): return self.jid_vers.get(jid, None) def get_caps(self, jid, node, ifrom, data): return self.ver_cache.get(data.get('verstring', None), None) slixmpp-1.2.2/slixmpp/plugins/xep_0115/stanza.py0000644000175000001440000000070013004224717022462 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import unicode_literals from slixmpp.xmlstream import ElementBase class Capabilities(ElementBase): namespace = 'http://jabber.org/protocol/caps' name = 'c' plugin_attrib = 'caps' interfaces = {'hash', 'node', 'ver', 'ext'} slixmpp-1.2.2/slixmpp/plugins/xep_0152/0000755000175000001440000000000013014656513020620 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0152/__init__.py0000644000175000001440000000065312424504520022727 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0152 import stanza from slixmpp.plugins.xep_0152.stanza import Reachability from slixmpp.plugins.xep_0152.reachability import XEP_0152 register_plugin(XEP_0152) slixmpp-1.2.2/slixmpp/plugins/xep_0152/stanza.py0000644000175000001440000000127713004224717022475 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Reachability(ElementBase): name = 'reach' namespace = 'urn:xmpp:reach:0' plugin_attrib = 'reach' interfaces = set() class Address(ElementBase): name = 'addr' namespace = 'urn:xmpp:reach:0' plugin_attrib = 'address' plugin_multi_attrib = 'addresses' interfaces = {'uri', 'desc'} lang_interfaces = {'desc'} sub_interfaces = {'desc'} register_stanza_plugin(Reachability, Address, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0152/reachability.py0000644000175000001440000000636313004224717023636 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0152 import stanza, Reachability log = logging.getLogger(__name__) class XEP_0152(BasePlugin): """ XEP-0152: Reachability Addresses """ name = 'xep_0152' description = 'XEP-0152: Reachability Addresses' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace) self.xmpp['xep_0163'].remove_interest(Reachability.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('reachability', Reachability) def publish_reachability(self, addresses, options=None, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Publish alternative addresses where the user can be reached. Arguments: addresses -- A list of dictionaries containing the URI and optional description for each address. options -- Optional form of publish options. ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ if not isinstance(addresses, (list, tuple)): addresses = [addresses] reach = Reachability() for address in addresses: if not hasattr(address, 'items'): address = {'uri': address} addr = stanza.Address() for key, val in address.items(): addr[key] = val reach.append(addr) return self.xmpp['xep_0163'].publish(reach, node=Reachability.namespace, options=options, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None): """ Clear existing user activity information to stop notifications. Arguments: ifrom -- Specify the sender's JID. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to slixmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ reach = Reachability() return self.xmpp['xep_0163'].publish(reach, node=Reachability.namespace, ifrom=ifrom, callback=callback, timeout=timeout, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0070/0000755000175000001440000000000013014656513020617 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0070/confirm.py0000644000175000001440000000512312755677265022651 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2015 Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ import asyncio import logging from uuid import uuid4 from slixmpp.plugins import BasePlugin, register_plugin from slixmpp import future_wrapper, Iq, Message from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.jid import JID from slixmpp.xmlstream import JID, register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0070 import stanza, Confirm log = logging.getLogger(__name__) class XEP_0070(BasePlugin): """ XEP-0070 Verifying HTTP Requests via XMPP """ name = 'xep_0070' description = 'XEP-0070: Verifying HTTP Requests via XMPP' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, Confirm) register_stanza_plugin(Message, Confirm) self.xmpp.register_handler( Callback('Confirm', StanzaPath('iq@type=get/confirm'), self._handle_iq_confirm)) self.xmpp.register_handler( Callback('Confirm', StanzaPath('message/confirm'), self._handle_message_confirm)) def plugin_end(self): self.xmpp.remove_handler('Confirm') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/http-auth') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/http-auth') @future_wrapper def ask_confirm(self, jid, id, url, method, *, ifrom=None, message=None): jid = JID(jid) if jid.resource: stanza = self.xmpp.Iq() stanza['type'] = 'get' else: stanza = self.xmpp.Message() stanza['thread'] = uuid4().hex stanza['from'] = ifrom stanza['to'] = jid stanza['confirm']['id'] = id stanza['confirm']['url'] = url stanza['confirm']['method'] = method if not jid.resource: if message is not None: stanza['body'] = message.format(id=id, url=url, method=method) stanza.send() return stanza else: return stanza.send() def _handle_iq_confirm(self, iq): self.xmpp.event('http_confirm_iq', iq) self.xmpp.event('http_confirm', iq) def _handle_message_confirm(self, message): self.xmpp.event('http_confirm_message', message) self.xmpp.event('http_confirm', message) slixmpp-1.2.2/slixmpp/plugins/xep_0070/__init__.py0000644000175000001440000000054412755677255022754 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2015 Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0070.stanza import Confirm from slixmpp.plugins.xep_0070.confirm import XEP_0070 register_plugin(XEP_0070) slixmpp-1.2.2/slixmpp/plugins/xep_0070/stanza.py0000644000175000001440000000061012755677255022507 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2015 Emmanuel Gil Peyrot This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase class Confirm(ElementBase): name = 'confirm' namespace = 'http://jabber.org/protocol/http-auth' plugin_attrib = 'confirm' interfaces = {'id', 'url', 'method'} slixmpp-1.2.2/slixmpp/plugins/xep_0082.py0000644000175000001440000001517012424504520021172 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import datetime as dt from slixmpp.plugins import BasePlugin, register_plugin from slixmpp.thirdparty import tzutc, tzoffset, parse_iso # ===================================================================== # To make it easier for stanzas without direct access to plugin objects # to use the XEP-0082 utility methods, we will define them as top-level # functions and then just reference them in the plugin itself. def parse(time_str): """ Convert a string timestamp into a datetime object. Arguments: time_str -- A formatted timestamp string. """ return parse_iso(time_str) def format_date(time_obj): """ Return a formatted string version of a date object. Format: YYYY-MM-DD Arguments: time_obj -- A date or datetime object. """ if isinstance(time_obj, dt.datetime): time_obj = time_obj.date() return time_obj.isoformat() def format_time(time_obj): """ Return a formatted string version of a time object. format: hh:mm:ss[.sss][TZD] arguments: time_obj -- A time or datetime object. """ if isinstance(time_obj, dt.datetime): time_obj = time_obj.timetz() timestamp = time_obj.isoformat() if time_obj.tzinfo == tzutc(): timestamp = timestamp[:-6] return '%sZ' % timestamp return timestamp def format_datetime(time_obj): """ Return a formatted string version of a datetime object. Format: YYYY-MM-DDThh:mm:ss[.sss]TZD arguments: time_obj -- A datetime object. """ timestamp = time_obj.isoformat('T') if time_obj.tzinfo == tzutc(): timestamp = timestamp[:-6] return '%sZ' % timestamp return timestamp def date(year=None, month=None, day=None, obj=False): """ Create a date only timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: year -- Integer value of the year (4 digits) month -- Integer value of the month day -- Integer value of the day of the month. obj -- If True, return the date object instead of a formatted string. Defaults to False. """ today = dt.datetime.utcnow() if year is None: year = today.year if month is None: month = today.month if day is None: day = today.day value = dt.date(year, month, day) if obj: return value return format_date(value) def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): """ Create a time only timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: hour -- Integer value of the hour. min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. obj -- If True, return the time object instead of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if hour is None: hour = now.hour if min is None: min = now.minute if sec is None: sec = now.second if micro is None: micro = now.microsecond if offset is None: offset = tzutc() elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) value = dt.time(hour, min, sec, micro, offset) if obj: return value return format_time(value) def datetime(year=None, month=None, day=None, hour=None, min=None, sec=None, micro=None, offset=None, separators=True, obj=False): """ Create a datetime timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: year -- Integer value of the year (4 digits) month -- Integer value of the month day -- Integer value of the day of the month. hour -- Integer value of the hour. min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. obj -- If True, return the datetime object instead of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if year is None: year = now.year if month is None: month = now.month if day is None: day = now.day if hour is None: hour = now.hour if min is None: min = now.minute if sec is None: sec = now.second if micro is None: micro = now.microsecond if offset is None: offset = tzutc() elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) value = dt.datetime(year, month, day, hour, min, sec, micro, offset) if obj: return value return format_datetime(value) class XEP_0082(BasePlugin): """ XEP-0082: XMPP Date and Time Profiles XMPP uses a subset of the formats allowed by ISO 8601 as a matter of pragmatism based on the relatively few formats historically used by the XMPP. Also see . Methods: date -- Create a time stamp using the Date profile. datetime -- Create a time stamp using the DateTime profile. time -- Create a time stamp using the Time profile. format_date -- Format an existing date object. format_datetime -- Format an existing datetime object. format_time -- Format an existing time object. parse -- Convert a time string into a Python datetime object. """ name = 'xep_0082' description = 'XEP-0082: XMPP Date and Time Profiles' dependencies = set() def plugin_init(self): """Start the XEP-0082 plugin.""" self.date = date self.datetime = datetime self.time = time self.format_date = format_date self.format_datetime = format_datetime self.format_time = format_time self.parse = parse register_plugin(XEP_0082) slixmpp-1.2.2/slixmpp/plugins/xep_0186/0000755000175000001440000000000013014656513020627 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0186/__init__.py0000644000175000001440000000066612424504520022742 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0186 import stanza from slixmpp.plugins.xep_0186.stanza import Invisible, Visible from slixmpp.plugins.xep_0186.invisible_command import XEP_0186 register_plugin(XEP_0186) slixmpp-1.2.2/slixmpp/plugins/xep_0186/invisible_command.py0000644000175000001440000000226713004224717024666 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0186 import stanza, Visible, Invisible log = logging.getLogger(__name__) class XEP_0186(BasePlugin): name = 'xep_0186' description = 'XEP-0186: Invisible Command' dependencies = {'xep_0030'} def plugin_init(self): register_stanza_plugin(Iq, Visible) register_stanza_plugin(Iq, Invisible) def set_invisible(self, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('invisible') return iq.send(callback=callback, timeout=timeout) def set_visible(self, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('visible') return iq.send(callback=callback, timeout=timeout) slixmpp-1.2.2/slixmpp/plugins/xep_0186/stanza.py0000644000175000001440000000101512424504520022470 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.xmlstream import ElementBase, ET class Invisible(ElementBase): name = 'invisible' namespace = 'urn:xmpp:invisible:0' plugin_attrib = 'invisible' interfaces = set() class Visible(ElementBase): name = 'visible' namespace = 'urn:xmpp:visible:0' plugin_attrib = 'visible' interfaces = set() slixmpp-1.2.2/slixmpp/plugins/xep_0016/0000755000175000001440000000000013014656513020617 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0016/__init__.py0000644000175000001440000000064112424504517022731 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0016 import stanza from slixmpp.plugins.xep_0016.stanza import Privacy from slixmpp.plugins.xep_0016.privacy import XEP_0016 register_plugin(XEP_0016) slixmpp-1.2.2/slixmpp/plugins/xep_0016/stanza.py0000644000175000001440000000577413004224717022502 0ustar mathieuiusers00000000000000from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Privacy(ElementBase): name = 'query' namespace = 'jabber:iq:privacy' plugin_attrib = 'privacy' interfaces = set() def add_list(self, name): priv_list = List() priv_list['name'] = name self.append(priv_list) return priv_list class Active(ElementBase): name = 'active' namespace = 'jabber:iq:privacy' plugin_attrib = name interfaces = {'name'} class Default(ElementBase): name = 'default' namespace = 'jabber:iq:privacy' plugin_attrib = name interfaces = {'name'} class List(ElementBase): name = 'list' namespace = 'jabber:iq:privacy' plugin_attrib = name plugin_multi_attrib = 'lists' interfaces = {'name'} def add_item(self, value, action, order, itype=None, iq=False, message=False, presence_in=False, presence_out=False): item = Item() item.values = {'type': itype, 'value': value, 'action': action, 'order': order, 'message': message, 'iq': iq, 'presence_in': presence_in, 'presence_out': presence_out} self.append(item) return item class Item(ElementBase): name = 'item' namespace = 'jabber:iq:privacy' plugin_attrib = name plugin_multi_attrib = 'items' interfaces = {'type', 'value', 'action', 'order', 'iq', 'message', 'presence_in', 'presence_out'} bool_interfaces = {'message', 'iq', 'presence_in', 'presence_out'} type_values = ('', 'jid', 'group', 'subscription') action_values = ('allow', 'deny') def set_type(self, value): if value and value not in self.type_values: raise ValueError('Unknown type value: %s' % value) else: self._set_attr('type', value) def set_action(self, value): if value not in self.action_values: raise ValueError('Unknown action value: %s' % value) else: self._set_attr('action', value) def set_presence_in(self, value): keep = True if value else False self._set_sub_text('presence-in', '', keep=keep) def get_presence_in(self): pres = self.xml.find('{%s}presence-in' % self.namespace) return pres is not None def del_presence_in(self): self._del_sub('{%s}presence-in' % self.namespace) def set_presence_out(self, value): keep = True if value else False self._set_sub_text('presence-in', '', keep=keep) def get_presence_out(self): pres = self.xml.find('{%s}presence-in' % self.namespace) return pres is not None def del_presence_out(self): self._del_sub('{%s}presence-in' % self.namespace) register_stanza_plugin(Privacy, Active) register_stanza_plugin(Privacy, Default) register_stanza_plugin(Privacy, List, iterable=True) register_stanza_plugin(List, Item, iterable=True) slixmpp-1.2.2/slixmpp/plugins/xep_0016/privacy.py0000644000175000001440000001047713004224717022653 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0016 import stanza from slixmpp.plugins.xep_0016.stanza import Privacy, Item class XEP_0016(BasePlugin): name = 'xep_0016' description = 'XEP-0016: Privacy Lists' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, Privacy) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Privacy.namespace) def get_privacy_lists(self, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq.enable('privacy') iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def get_list(self, name, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy']['list']['name'] = name iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def get_active(self, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy'].enable('active') iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def get_default(self, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy'].enable('default') iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def activate(self, name, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['active']['name'] = name iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def deactivate(self, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy'].enable('active') iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def make_default(self, name, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['default']['name'] = name iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def remove_default(self, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy'].enable('default') iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) def edit_list(self, name, rules, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['list']['name'] = name priv_list = iq['privacy']['list'] if not rules: rules = [] for rule in rules: if isinstance(rule, Item): priv_list.append(rule) continue priv_list.add_item( rule['value'], rule['action'], rule['order'], itype=rule.get('type', None), iq=rule.get('iq', None), message=rule.get('message', None), presence_in=rule.get('presence_in', rule.get('presence-in', None)), presence_out=rule.get('presence_out', rule.get('presence-out', None))) def remove_list(self, name, timeout=None, callback=None, timeout_callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['list']['name'] = name iq.send(timeout=timeout, callback=callback, timeout_callback=timeout_callback) slixmpp-1.2.2/slixmpp/plugins/xep_0202/0000755000175000001440000000000013014656513020614 5ustar mathieuiusers00000000000000slixmpp-1.2.2/slixmpp/plugins/xep_0202/time.py0000644000175000001440000000565713004224717022135 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging from slixmpp.stanza.iq import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins import xep_0082 from slixmpp.plugins.xep_0202 import stanza log = logging.getLogger(__name__) class XEP_0202(BasePlugin): """ XEP-0202: Entity Time """ name = 'xep_0202' description = 'XEP-0202: Entity Time' dependencies = {'xep_0030', 'xep_0082'} stanza = stanza default_config = { #: As a default, respond to time requests with the #: local time returned by XEP-0082. However, a #: custom function can be supplied which accepts #: the JID of the entity to query for the time. 'local_time': None, 'tz_offset': 0 } def plugin_init(self): """Start the XEP-0203 plugin.""" if not self.local_time: def default_local_time(jid): return xep_0082.datetime(offset=self.tz_offset) self.local_time = default_local_time self.xmpp.register_handler( Callback('Entity Time', StanzaPath('iq/entity_time'), self._handle_time_request)) register_stanza_plugin(Iq, stanza.EntityTime) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:time') self.xmpp.remove_handler('Entity Time') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:time') def _handle_time_request(self, iq): """ Respond to a request for the local time. The time is taken from self.local_time(), which may be replaced during plugin configuration with a function that maps JIDs to times. Arguments: iq -- The Iq time request stanza. """ iq = iq.reply() iq['entity_time']['time'] = self.local_time(iq['to']) iq.send() def get_entity_time(self, to, ifrom=None, **iqargs): """ Request the time from another entity. Arguments: to -- JID of the entity to query. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. """ iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = to iq['from'] = ifrom iq.enable('entity_time') return iq.send(**iqargs) slixmpp-1.2.2/slixmpp/plugins/xep_0202/__init__.py0000644000175000001440000000064112770302340022720 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0202 import stanza from slixmpp.plugins.xep_0202.stanza import EntityTime from slixmpp.plugins.xep_0202.time import XEP_0202 register_plugin(XEP_0202) slixmpp-1.2.2/slixmpp/plugins/xep_0202/stanza.py0000644000175000001440000000713213004224717022465 0ustar mathieuiusers00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of Slixmpp. See the file LICENSE for copying permission. """ import logging import datetime as dt from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 from slixmpp.thirdparty import tzutc, tzoffset class EntityTime(ElementBase): """ The