From 17f08929f91958fb88c504eb3eaf4597f9c044ba Mon Sep 17 00:00:00 2001 From: mathieui Date: Wed, 3 Feb 2021 22:34:33 +0100 Subject: docs: add slixmpp credits, move fork note to the bottom --- docs/index.rst | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/index.rst b/docs/index.rst index a01109cd..1a7b2043 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,11 +16,6 @@ Slixmpp **Reporting bugs** You can report bugs at http://lab.louiz.org/poezio/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.7+, Slixmpp's design goals and philosphy are: @@ -200,9 +195,30 @@ Additional Info * :ref:`modindex` * :ref:`search` + +Slixmpp Credits +--------------- + +**Maintainers:** + - Florent Le Coz (`louiz@louiz.org `_), + - Mathieu Pasquet (`mathieui@mathieui.net `_), + - Emmanuel Gil Peyrot (`Link mauve `_) + - Maxime Buquet (`pep `_) + +**Contributors:** + - Sam Whited (`Sam Whited `_) + - Dan Sully (`Dan Sully `_) + - Gasper Zejn (`Gasper Zejn `_) + - Krzysztof Kotlenga (`Krzysztof Kotlenga `_) + - Tsukasa Hiiragi (`Tsukasa Hiiragi `_) + SleekXMPP Credits ----------------- +Slixmpp is a friendly fork of `SleekXMPP `_ +which goal is to use asyncio instead of threads to handle networking. See +:ref:`differences`. We are crediting SleekXMPP Authors here. + .. 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 -- cgit v1.2.3 From d9975aa4c0af1516ec00b66f90bc368a7133ffa6 Mon Sep 17 00:00:00 2001 From: mathieui Date: Wed, 3 Feb 2021 23:04:02 +0100 Subject: docs: move things around for a cleaner toctree --- docs/api/index.rst | 16 + docs/api/stanza/index.rst | 11 + docs/create_plugin.rst | 682 ---------------------------------------- docs/features.rst | 2 - docs/getting_started/index.rst | 15 + docs/guide_xep_0030.rst | 200 ------------ docs/handlersmatchers.rst | 4 - docs/howto/create_plugin.rst | 682 ++++++++++++++++++++++++++++++++++++++++ docs/howto/features.rst | 2 + docs/howto/guide_xep_0030.rst | 200 ++++++++++++ docs/howto/handlersmatchers.rst | 4 + docs/howto/index.rst | 13 + docs/howto/sasl.rst | 2 + docs/howto/xeps.rst | 50 +++ docs/howto/xmpp_tdg.rst | 249 +++++++++++++++ docs/index.rst | 83 +---- docs/plugin_arch.rst | 2 - docs/sasl.rst | 2 - docs/sleekxmpp.rst | 8 + docs/xeps.rst | 50 --- docs/xmpp_tdg.rst | 249 --------------- 21 files changed, 1260 insertions(+), 1266 deletions(-) create mode 100644 docs/api/index.rst create mode 100644 docs/api/stanza/index.rst delete mode 100644 docs/create_plugin.rst delete mode 100644 docs/features.rst create mode 100644 docs/getting_started/index.rst delete mode 100644 docs/guide_xep_0030.rst delete mode 100644 docs/handlersmatchers.rst create mode 100644 docs/howto/create_plugin.rst create mode 100644 docs/howto/features.rst create mode 100644 docs/howto/guide_xep_0030.rst create mode 100644 docs/howto/handlersmatchers.rst create mode 100644 docs/howto/index.rst create mode 100644 docs/howto/sasl.rst create mode 100644 docs/howto/xeps.rst create mode 100644 docs/howto/xmpp_tdg.rst delete mode 100644 docs/plugin_arch.rst delete mode 100644 docs/sasl.rst create mode 100644 docs/sleekxmpp.rst delete mode 100644 docs/xeps.rst delete mode 100644 docs/xmpp_tdg.rst (limited to 'docs') diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..aefa0f56 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,16 @@ +API Reference +------------- + +.. toctree:: + :maxdepth: 3 + + clientxmpp + componentxmpp + basexmpp + exceptions + xmlstream/jid + xmlstream/stanzabase + xmlstream/handler + xmlstream/matcher + xmlstream/xmlstream + xmlstream/tostring diff --git a/docs/api/stanza/index.rst b/docs/api/stanza/index.rst new file mode 100644 index 00000000..2af0d630 --- /dev/null +++ b/docs/api/stanza/index.rst @@ -0,0 +1,11 @@ + +Core Stanzas +------------ + +.. toctree:: + :maxdepth: 2 + + rootstanza + message + presence + iq diff --git a/docs/create_plugin.rst b/docs/create_plugin.rst deleted file mode 100644 index 437374c7..00000000 --- a/docs/create_plugin.rst +++ /dev/null @@ -1,682 +0,0 @@ -.. _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() diff --git a/docs/features.rst b/docs/features.rst deleted file mode 100644 index 4d93d5c3..00000000 --- a/docs/features.rst +++ /dev/null @@ -1,2 +0,0 @@ -How to Use Stream Features -========================== diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst new file mode 100644 index 00000000..97c34a22 --- /dev/null +++ b/docs/getting_started/index.rst @@ -0,0 +1,15 @@ +Getting Started (with examples) +------------------------------- + +.. toctree:: + :maxdepth: 3 + + echobot + sendlogout + component + presence + muc + proxy + scheduler + iq + diff --git a/docs/guide_xep_0030.rst b/docs/guide_xep_0030.rst deleted file mode 100644 index a3af4857..00000000 --- a/docs/guide_xep_0030.rst +++ /dev/null @@ -1,200 +0,0 @@ -XEP-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) diff --git a/docs/handlersmatchers.rst b/docs/handlersmatchers.rst deleted file mode 100644 index 628c4142..00000000 --- a/docs/handlersmatchers.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. _using-handlers-matchers: - -Using Stream Handlers and Matchers -================================== diff --git a/docs/howto/create_plugin.rst b/docs/howto/create_plugin.rst new file mode 100644 index 00000000..437374c7 --- /dev/null +++ b/docs/howto/create_plugin.rst @@ -0,0 +1,682 @@ +.. _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() diff --git a/docs/howto/features.rst b/docs/howto/features.rst new file mode 100644 index 00000000..4d93d5c3 --- /dev/null +++ b/docs/howto/features.rst @@ -0,0 +1,2 @@ +How to Use Stream Features +========================== diff --git a/docs/howto/guide_xep_0030.rst b/docs/howto/guide_xep_0030.rst new file mode 100644 index 00000000..a3af4857 --- /dev/null +++ b/docs/howto/guide_xep_0030.rst @@ -0,0 +1,200 @@ +XEP-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) diff --git a/docs/howto/handlersmatchers.rst b/docs/howto/handlersmatchers.rst new file mode 100644 index 00000000..628c4142 --- /dev/null +++ b/docs/howto/handlersmatchers.rst @@ -0,0 +1,4 @@ +.. _using-handlers-matchers: + +Using Stream Handlers and Matchers +================================== diff --git a/docs/howto/index.rst b/docs/howto/index.rst new file mode 100644 index 00000000..b05dc499 --- /dev/null +++ b/docs/howto/index.rst @@ -0,0 +1,13 @@ +Tutorials, FAQs, and How To Guides +---------------------------------- + +.. toctree:: + :maxdepth: 2 + + stanzas + create_plugin + features + sasl + handlersmatchers + guide_xep_0030 + xmpp_tdg diff --git a/docs/howto/sasl.rst b/docs/howto/sasl.rst new file mode 100644 index 00000000..46c45c2a --- /dev/null +++ b/docs/howto/sasl.rst @@ -0,0 +1,2 @@ +How SASL Authentication Works +============================= diff --git a/docs/howto/xeps.rst b/docs/howto/xeps.rst new file mode 100644 index 00000000..c5b4592d --- /dev/null +++ b/docs/howto/xeps.rst @@ -0,0 +1,50 @@ +Supported 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 diff --git a/docs/howto/xmpp_tdg.rst b/docs/howto/xmpp_tdg.rst new file mode 100644 index 00000000..53194e13 --- /dev/null +++ b/docs/howto/xmpp_tdg.rst @@ -0,0 +1,249 @@ +Following *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 (1) `_ | +`View original code (1) `_ + +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 (2) `_ | +`View original code (2) `_ + + +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 (3) `_ | +`View original code (3) `_ + + +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 (4) `_ | +`View original code (4) `_ + + +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 (5) `_ | +`View original code (5) `_ + +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 (6) `_ | +`View original code (6) `_ diff --git a/docs/index.rst b/docs/index.rst index 1a7b2043..d088e015 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -93,94 +93,27 @@ Here's your first Slixmpp Bot: xmpp.process() -To read if you come from SleekXMPP ----------------------------------- +Documentation Index +------------------- -.. 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 + getting_started/index + howto/index + api/index + api/stanza/index 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 + sleekxmpp + architecture Plugins ~~~~~~~ - .. toctree:: :maxdepth: 1 api/plugins/index -Core Stanzas -~~~~~~~~~~~~ -.. toctree:: - :maxdepth: 2 - - api/stanza/rootstanza - api/stanza/message - api/stanza/presence - api/stanza/iq - Additional Info --------------- .. toctree:: diff --git a/docs/plugin_arch.rst b/docs/plugin_arch.rst deleted file mode 100644 index 0141b793..00000000 --- a/docs/plugin_arch.rst +++ /dev/null @@ -1,2 +0,0 @@ -Plugin Architecture -=================== diff --git a/docs/sasl.rst b/docs/sasl.rst deleted file mode 100644 index 46c45c2a..00000000 --- a/docs/sasl.rst +++ /dev/null @@ -1,2 +0,0 @@ -How SASL Authentication Works -============================= diff --git a/docs/sleekxmpp.rst b/docs/sleekxmpp.rst new file mode 100644 index 00000000..12f02377 --- /dev/null +++ b/docs/sleekxmpp.rst @@ -0,0 +1,8 @@ +Coming from SleekXMPP +--------------------- + +.. toctree:: + :maxdepth: 2 + + differences + using_asyncio diff --git a/docs/xeps.rst b/docs/xeps.rst deleted file mode 100644 index c5b4592d..00000000 --- a/docs/xeps.rst +++ /dev/null @@ -1,50 +0,0 @@ -Supported 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 diff --git a/docs/xmpp_tdg.rst b/docs/xmpp_tdg.rst deleted file mode 100644 index 53194e13..00000000 --- a/docs/xmpp_tdg.rst +++ /dev/null @@ -1,249 +0,0 @@ -Following *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 (1) `_ | -`View original code (1) `_ - -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 (2) `_ | -`View original code (2) `_ - - -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 (3) `_ | -`View original code (3) `_ - - -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 (4) `_ | -`View original code (4) `_ - - -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 (5) `_ | -`View original code (5) `_ - -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 (6) `_ | -`View original code (6) `_ -- cgit v1.2.3 From f8c39bae60d7b9125ffc5d2c90ce59147fb46785 Mon Sep 17 00:00:00 2001 From: mathieui Date: Wed, 3 Feb 2021 23:17:37 +0100 Subject: docs: update the iq tutorial --- docs/getting_started/iq.rst | 55 ++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 26 deletions(-) (limited to 'docs') diff --git a/docs/getting_started/iq.rst b/docs/getting_started/iq.rst index be15e170..aac75712 100644 --- a/docs/getting_started/iq.rst +++ b/docs/getting_started/iq.rst @@ -3,7 +3,7 @@ Send/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 +text data for basic usage, :class:`~slixmpp.stanza.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. @@ -11,7 +11,7 @@ make working with them easier. Basic Use --------- -XMPP's use of :class:`~slixmpp.stanza.iq.Iq` stanzas is built around +XMPP's use of :class:`~slixmpp.stanza.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 @@ -26,7 +26,7 @@ need to be able to send: Creating Iq Stanzas ~~~~~~~~~~~~~~~~~~~ -Slixmpp provides built-in support for creating basic :class:`~slixmpp.stanza.iq.Iq` +Slixmpp provides built-in support for creating basic :class:`~slixmpp.stanza.Iq` stanzas this way. The relevant methods are: * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq` @@ -37,7 +37,7 @@ stanzas this way. The relevant methods are: * :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 +:class:`~slixmpp.stanza.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: @@ -50,18 +50,16 @@ namespace. For example, to produce the query above, you would use: 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()` +Once an :class:`~slixmpp.stanza.Iq` stanza is created, sending it +over the wire is done using its :meth:`~slixmpp.stanza.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. +options to control how to wait for the query's response, as well as +how to handle the result. -These options are: +:meth:`~slixmpp.stanza.Iq.send()` returns an :class:`~asyncio.Future` +object, which can be awaited on until a ``result`` is received. -* ``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. +These options are: * ``timeout``: When using the blocking behaviour, the call will eventually timeout with an error. The default timeout is 30 seconds, but this may @@ -79,28 +77,33 @@ These options are: * ``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. + whenever a response is finally received. .. code-block:: python - cb_name = iq.send(callback=self.a_callback) + iq.send(callback=self.a_callback) + +* ``timeout_callback``: A callback to execute when the provided + ``timeout`` is reached before an answer is received. + + +.. note:: - # ... later if we need to cancel - self.remove_handler(cb_name) + Both ``callback`` and ``timeout_callback`` can be effectively + replaced using ``await``, and standard exception handling + (see below), which provide a more linear and readable workflow. -Properly working with :class:`~slixmpp.stanza.iq.Iq` stanzas requires +Properly working with :class:`~slixmpp.stanza.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` +:meth:`~slixmpp.stanza.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() + resp = await iq.send() # ... do stuff with expected Iq result except IqError as e: err_resp = e.iq @@ -116,7 +119,7 @@ exception: .. code-block:: python try: - resp = iq.send() + resp = await iq.send() except XMPPError: # ... Don't care about the response pass @@ -126,7 +129,7 @@ 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. +interfacting with the :class:`~slixmpp.stanza.Iq` payload. .. seealso:: @@ -135,13 +138,13 @@ interfacting with the :class:`~slixmpp.stanza.iq.Iq` payload. * :ref:`using-handlers-matchers` -The typical way to respond to :class:`~slixmpp.stanza.iq.Iq` requests is +The typical way to respond to :class:`~slixmpp.stanza.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: +There are two types of incoming :class:`~slixmpp.stanza.Iq` requests: ``get`` and ``set``. You can register a handler that will accept both and then filter by type as needed, as so: -- cgit v1.2.3