1.. _create-plugin:
2
3Creating a Slixmpp Plugin
4===========================
5
6One of the goals of Slixmpp is to provide support for every draft or final
7XMPP extension (`XEP <http://xmpp.org/extensions/>`_). To do this, Slixmpp has a
8plugin mechanism for adding the functionalities required by each XEP. But even
9though plugins were made to quickly implement and prototype the official XMPP
10extensions, there is no reason you can't create your own plugin to implement
11your own custom XMPP-based protocol.
12
13This guide will help walk you through the steps to
14implement a rudimentary version of `XEP-0077 In-band
15Registration <http://xmpp.org/extensions/xep-0077.html>`_. In-band registration
16was implemented in example 14-6 (page 223) of `XMPP: The Definitive
17Guide <http://oreilly.com/catalog/9780596521271>`_ because there was no Slixmpp
18plugin for XEP-0077 at the time of writing. We will partially fix that issue
19here by turning the example implementation from *XMPP: The Definitive Guide*
20into a plugin. Again, note that this will not a complete implementation, and a
21different, more robust, official plugin for XEP-0077 may be added to Slixmpp
22in the future.
23
24.. note::
25
26    The example plugin created in this guide is for the server side of the
27    registration process only. It will **NOT** be able to register new accounts
28    on an XMPP server.
29
30First Steps
31-----------
32Every plugin inherits from the class :mod:`BasePlugin <slixmpp.plugins.base.BasePlugin`,
33and must include a ``plugin_init`` method. While the
34plugins distributed with Slixmpp must be placed in the plugins directory
35``slixmpp/plugins`` to be loaded, custom plugins may be loaded from any
36module. To do so, use the following form when registering the plugin:
37
38.. code-block:: python
39
40    self.register_plugin('myplugin', module=mod_containing_my_plugin)
41
42The plugin name must be the same as the plugin's class name.
43
44Now, we can open our favorite text editors and create ``xep_0077.py`` in
45``Slixmpp/slixmpp/plugins``. We want to do some basic house-keeping and
46declare the name and description of the XEP we are implementing. If you
47are creating your own custom plugin, you don't need to include the ``xep``
48attribute.
49
50.. code-block:: python
51
52    """
53    Creating a Slixmpp Plugin
54
55    This is a minimal implementation of XEP-0077 to serve
56    as a tutorial for creating Slixmpp plugins.
57    """
58
59    from slixmpp.plugins.base import BasePlugin
60
61    class xep_0077(BasePlugin):
62        """
63        XEP-0077 In-Band Registration
64        """
65
66        def plugin_init(self):
67            self.description = "In-Band Registration"
68            self.xep = "0077"
69
70Now that we have a basic plugin, we need to edit
71``slixmpp/plugins/__init__.py`` to include our new plugin by adding
72``'xep_0077'`` to the ``__all__`` declaration.
73
74Interacting with Other Plugins
75------------------------------
76
77In-band registration is a feature that should be advertised through `Service
78Discovery <http://xmpp.org/extensions/xep-0030.html>`_. To do that, we tell the
79``xep_0030`` plugin to add the ``"jabber:iq:register"`` feature. We put this
80call in a method named ``post_init`` which will be called once the plugin has
81been loaded; by doing so we advertise that we can do registrations only after we
82finish activating the plugin.
83
84The ``post_init`` method needs to call ``BasePlugin.post_init(self)``
85which will mark that ``post_init`` has been called for the plugin. Once the
86Slixmpp object begins processing, ``post_init`` will be called on any plugins
87that have not already run ``post_init``. This allows you to register plugins and
88their dependencies without needing to worry about the order in which you do so.
89
90**Note:** by adding this call we have introduced a dependency on the XEP-0030
91plugin. Be sure to register ``'xep_0030'`` as well as ``'xep_0077'``. Slixmpp
92does not automatically load plugin dependencies for you.
93
94.. code-block:: python
95
96    def post_init(self):
97        BasePlugin.post_init(self)
98        self.xmpp['xep_0030'].add_feature("jabber:iq:register")
99
100Creating Custom Stanza Objects
101------------------------------
102
103Now, the IQ stanzas needed to implement our version of XEP-0077 are not very
104complex, and we could just interact with the XML objects directly just like
105in the *XMPP: The Definitive Guide* example. However, creating custom stanza
106objects is good practice.
107
108We will create a new ``Registration`` stanza. Following the *XMPP: The
109Definitive Guide* example, we will add support for a username and password
110field. We also need two flags: ``registered`` and ``remove``. The ``registered``
111flag is sent when an already registered user attempts to register, along with
112their registration data. The ``remove`` flag is a request to unregister a user's
113account.
114
115Adding additional `fields specified in
116XEP-0077 <http://xmpp.org/extensions/xep-0077.html#registrar-formtypes-register>`_
117will not be difficult and is left as an exercise for the reader.
118
119Our ``Registration`` class needs to start with a few descriptions of its
120behaviour:
121
122* ``namespace``
123    The namespace our stanza object lives in. In this case,
124    ``"jabber:iq:register"``.
125
126* ``name``
127    The name of the root XML element. In this case, the ``query`` element.
128
129* ``plugin_attrib``
130    The name to access this type of stanza. In particular, given a
131    registration stanza, the ``Registration`` object can be found using:
132    ``iq_object['register']``.
133
134* ``interfaces``
135    A list of dictionary-like keys that can be used with the stanza object.
136    When using ``"key"``, if there exists a method of the form ``getKey``,
137    ``setKey``, or``delKey`` (depending on context) then the result of calling
138    that method will be returned. Otherwise, the value of the attribute ``key``
139    of the main stanza element is returned if one exists.
140
141    **Note:** The accessor methods currently use title case, and not camel case.
142    Thus if you need to access an item named ``"methodName"`` you will need to
143    use ``getMethodname``. This naming convention might change to full camel
144    case in a future version of Slixmpp.
145
146* ``sub_interfaces``
147    A subset of ``interfaces``, but these keys map to the text of any
148    subelements that are direct children of the main stanza element. Thus,
149    referencing ``iq_object['register']['username']`` will either execute
150    ``getUsername`` or return the value in the ``username`` element of the
151    query.
152
153    If you need to access an element, say ``elem``, that is not a direct child
154    of the main stanza element, you will need to add ``getElem``, ``setElem``,
155    and ``delElem``. See the note above about naming conventions.
156
157.. code-block:: python
158
159    from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
160    from slixmpp import Iq
161
162    class Registration(ElementBase):
163        namespace = 'jabber:iq:register'
164        name = 'query'
165        plugin_attrib = 'register'
166        interfaces = {'username', 'password', 'registered', 'remove'}
167        sub_interfaces = interfaces
168
169        def getRegistered(self):
170            present = self.xml.find('{%s}registered' % self.namespace)
171            return present is not None
172
173        def getRemove(self):
174            present = self.xml.find('{%s}remove' % self.namespace)
175            return present is not None
176
177        def setRegistered(self, registered):
178            if registered:
179                self.addField('registered')
180            else:
181                del self['registered']
182
183        def setRemove(self, remove):
184            if remove:
185                self.addField('remove')
186            else:
187                del self['remove']
188
189        def addField(self, name):
190            itemXML = ET.Element('{%s}%s' % (self.namespace, name))
191            self.xml.append(itemXML)
192
193Setting a ``sub_interface`` attribute to ``""`` will remove that subelement.
194Since we want to include empty registration fields in our form, we need the
195``addField`` method to add the empty elements.
196
197Since the ``registered`` and ``remove`` elements are just flags, we need to add
198custom logic to enforce the binary behavior.
199
200Extracting Stanzas from the XML Stream
201--------------------------------------
202
203Now that we have a custom stanza object, we need to be able to detect when we
204receive one. To do this, we register a stream handler that will pattern match
205stanzas off of the XML stream against our stanza object's element name and
206namespace. To do so, we need to create a ``Callback`` object which contains
207an XML fragment that can identify our stanza type. We can add this handler
208registration to our ``plugin_init`` method.
209
210Also, we need to associate our ``Registration`` class with IQ stanzas;
211that requires the use of the ``register_stanza_plugin`` function (in
212``slixmpp.xmlstream.stanzabase``) which takes the class of a parent stanza
213type followed by the substanza type. In our case, the parent stanza is an IQ
214stanza, and the substanza is our registration query.
215
216The ``__handleRegistration`` method referenced in the callback will be our
217handler function to process registration requests.
218
219.. code-block:: python
220
221    def plugin_init(self):
222        self.description = "In-Band Registration"
223        self.xep = "0077"
224
225        self.xmpp.register_handler(
226          Callback('In-Band Registration',
227            MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns),
228            self.__handleRegistration))
229        register_stanza_plugin(Iq, Registration)
230
231Handling Incoming Stanzas and Triggering Events
232-----------------------------------------------
233There are six situations that we need to handle to finish our implementation of
234XEP-0077.
235
236**Registration Form Request from a New User:**
237
238    .. code-block:: xml
239
240        <iq type="result">
241         <query xmlns="jabber:iq:register">
242          <username />
243          <password />
244         </query>
245        </iq>
246
247**Registration Form Request from an Existing User:**
248
249    .. code-block:: xml
250
251        <iq type="result">
252         <query xmlns="jabber:iq:register">
253          <registered />
254          <username>Foo</username>
255          <password>hunter2</password>
256         </query>
257        </iq>
258
259**Unregister Account:**
260
261    .. code-block:: xml
262
263        <iq type="result">
264         <query xmlns="jabber:iq:register" />
265        </iq>
266
267**Incomplete Registration:**
268
269    .. code-block:: xml
270
271        <iq type="error">
272          <query xmlns="jabber:iq:register">
273            <username>Foo</username>
274          </query>
275         <error code="406" type="modify">
276          <not-acceptable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
277         </error>
278        </iq>
279
280**Conflicting Registrations:**
281
282    .. code-block:: xml
283
284        <iq type="error">
285         <query xmlns="jabber:iq:register">
286          <username>Foo</username>
287          <password>hunter2</password>
288         </query>
289         <error code="409" type="cancel">
290          <conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
291         </error>
292        </iq>
293
294**Successful Registration:**
295
296    .. code-block:: xml
297
298        <iq type="result">
299         <query xmlns="jabber:iq:register" />
300        </iq>
301
302Cases 1 and 2: Registration Requests
303~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
304Responding to registration requests depends on if the requesting user already
305has an account. If there is an account, the response should include the
306``registered`` flag and the user's current registration information. Otherwise,
307we just send the fields for our registration form.
308
309We will handle both cases by creating a ``sendRegistrationForm`` method that
310will create either an empty of full form depending on if we provide it with
311user data. Since we need to know which form fields to include (especially if we
312add support for the other fields specified in XEP-0077), we will also create a
313method ``setForm`` which will take the names of the fields we wish to include.
314
315.. code-block:: python
316
317    def plugin_init(self):
318        self.description = "In-Band Registration"
319        self.xep = "0077"
320        self.form_fields = ('username', 'password')
321        ... remainder of plugin_init
322
323    ...
324
325    def __handleRegistration(self, iq):
326        if iq['type'] == 'get':
327            # Registration form requested
328            userData = self.backend[iq['from'].bare]
329            self.sendRegistrationForm(iq, userData)
330
331    def setForm(self, *fields):
332        self.form_fields = fields
333
334    def sendRegistrationForm(self, iq, userData=None):
335        reg = iq['register']
336        if userData is None:
337            userData = {}
338        else:
339            reg['registered'] = True
340
341        for field in self.form_fields:
342            data = userData.get(field, '')
343            if data:
344                # Add field with existing data
345                reg[field] = data
346            else:
347                # Add a blank field
348                reg.addField(field)
349
350        iq.reply().set_payload(reg.xml)
351        iq.send()
352
353Note how we are able to access our ``Registration`` stanza object with
354``iq['register']``.
355
356A User Backend
357++++++++++++++
358You might have noticed the reference to ``self.backend``, which is an object
359that abstracts away storing and retrieving user information. Since it is not
360much more than a dictionary, we will leave the implementation details to the
361final, full source code example.
362
363Case 3: Unregister an Account
364~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
365The next simplest case to consider is responding to a request to remove
366an account. If we receive a ``remove`` flag, we instruct the backend to
367remove the user's account. Since your application may need to know about
368when users are registered or unregistered, we trigger an event using
369``self.xmpp.event('unregister_user', iq)``. See the component examples below for
370how to respond to that event.
371
372.. code-block:: python
373
374     def __handleRegistration(self, iq):
375        if iq['type'] == 'get':
376            # Registration form requested
377            userData = self.backend[iq['from'].bare]
378            self.sendRegistrationForm(iq, userData)
379        elif iq['type'] == 'set':
380            # Remove an account
381            if iq['register']['remove']:
382                self.backend.unregister(iq['from'].bare)
383                self.xmpp.event('unregistered_user', iq)
384                iq.reply().send()
385                return
386
387Case 4: Incomplete Registration
388~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
389For the next case we need to check the user's registration to ensure it has all
390of the fields we wanted. The simple option that we will use is to loop over the
391field names and check each one; however, this means that all fields we send to
392the user are required. Adding optional fields is left to the reader.
393
394Since we have received an incomplete form, we need to send an error message back
395to the user. We have to send a few different types of errors, so we will also
396create a ``_sendError`` method that will add the appropriate ``error`` element
397to the IQ reply.
398
399.. code-block:: python
400
401    def __handleRegistration(self, iq):
402        if iq['type'] == 'get':
403            # Registration form requested
404            userData = self.backend[iq['from'].bare]
405            self.sendRegistrationForm(iq, userData)
406        elif iq['type'] == 'set':
407            if iq['register']['remove']:
408                # Remove an account
409                self.backend.unregister(iq['from'].bare)
410                self.xmpp.event('unregistered_user', iq)
411                iq.reply().send()
412                return
413
414            for field in self.form_fields:
415                if not iq['register'][field]:
416                    # Incomplete Registration
417                    self._sendError(iq, '406', 'modify', 'not-acceptable'
418                                    "Please fill in all fields.")
419                    return
420
421    ...
422
423    def _sendError(self, iq, code, error_type, name, text=''):
424        iq.reply().set_payload(iq['register'].xml)
425        iq.error()
426        iq['error']['code'] = code
427        iq['error']['type'] = error_type
428        iq['error']['condition'] = name
429        iq['error']['text'] = text
430        iq.send()
431
432Cases 5 and 6: Conflicting and Successful Registration
433~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
434We are down to the final decision on if we have a successful registration. We
435send the user's data to the backend with the ``self.backend.register`` method.
436If it returns ``True``, then registration has been successful. Otherwise,
437there has been a conflict with usernames and registration has failed. Like
438with unregistering an account, we trigger an event indicating that a user has
439been registered by using ``self.xmpp.event('registered_user', iq)``. See the
440component examples below for how to respond to this event.
441
442.. code-block:: python
443
444    def __handleRegistration(self, iq):
445        if iq['type'] == 'get':
446            # Registration form requested
447            userData = self.backend[iq['from'].bare]
448            self.sendRegistrationForm(iq, userData)
449        elif iq['type'] == 'set':
450            if iq['register']['remove']:
451                # Remove an account
452                self.backend.unregister(iq['from'].bare)
453                self.xmpp.event('unregistered_user', iq)
454                iq.reply().send()
455                return
456
457            for field in self.form_fields:
458                if not iq['register'][field]:
459                    # Incomplete Registration
460                    self._sendError(iq, '406', 'modify', 'not-acceptable',
461                                    "Please fill in all fields.")
462                    return
463
464            if self.backend.register(iq['from'].bare, iq['register']):
465                # Successful registration
466                self.xmpp.event('registered_user', iq)
467                iq.reply().set_payload(iq['register'].xml)
468                iq.send()
469            else:
470                # Conflicting registration
471                self._sendError(iq, '409', 'cancel', 'conflict',
472                                "That username is already taken.")
473
474Example Component Using the XEP-0077 Plugin
475-------------------------------------------
476Alright, the moment we've been working towards - actually using our plugin to
477simplify our other applications. Here is a basic component that simply manages
478user registrations and sends the user a welcoming message when they register,
479and a farewell message when they delete their account.
480
481Note that we have to register the ``'xep_0030'`` plugin first,
482and that we specified the form fields we wish to use with
483``self.xmpp.plugin['xep_0077'].setForm('username', 'password')``.
484
485.. code-block:: python
486
487    import slixmpp.componentxmpp
488
489    class Example(slixmpp.componentxmpp.ComponentXMPP):
490
491        def __init__(self, jid, password):
492            slixmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 8888)
493
494            self.register_plugin('xep_0030')
495            self.register_plugin('xep_0077')
496            self.plugin['xep_0077'].setForm('username', 'password')
497
498            self.add_event_handler("registered_user", self.reg)
499            self.add_event_handler("unregistered_user", self.unreg)
500
501        def reg(self, iq):
502            msg = "Welcome! %s" % iq['register']['username']
503            self.send_message(iq['from'], msg, mfrom=self.fulljid)
504
505        def unreg(self, iq):
506            msg = "Bye! %s" % iq['register']['username']
507            self.send_message(iq['from'], msg, mfrom=self.fulljid)
508
509**Congratulations!** We now have a basic, functioning implementation of
510XEP-0077.
511
512Complete Source Code for XEP-0077 Plugin
513----------------------------------------
514Here is a copy of a more complete implementation of the plugin we created, but
515with some additional registration fields implemented.
516
517.. code-block:: python
518
519    """
520    Creating a Slixmpp Plugin
521
522    This is a minimal implementation of XEP-0077 to serve
523    as a tutorial for creating Slixmpp plugins.
524    """
525
526    from slixmpp.plugins.base import BasePlugin
527    from slixmpp.xmlstream.handler.callback import Callback
528    from slixmpp.xmlstream.matcher.xpath import MatchXPath
529    from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
530    from slixmpp import Iq
531    import copy
532
533
534    class Registration(ElementBase):
535        namespace = 'jabber:iq:register'
536        name = 'query'
537        plugin_attrib = 'register'
538        interfaces = {'username', 'password', 'email', 'nick', 'name',
539                      'first', 'last', 'address', 'city', 'state', 'zip',
540                      'phone', 'url', 'date', 'misc', 'text', 'key',
541                      'registered', 'remove', 'instructions'}
542        sub_interfaces = interfaces
543
544        def getRegistered(self):
545            present = self.xml.find('{%s}registered' % self.namespace)
546            return present is not None
547
548        def getRemove(self):
549            present = self.xml.find('{%s}remove' % self.namespace)
550            return present is not None
551
552        def setRegistered(self, registered):
553            if registered:
554                self.addField('registered')
555            else:
556                del self['registered']
557
558        def setRemove(self, remove):
559            if remove:
560                self.addField('remove')
561            else:
562                del self['remove']
563
564        def addField(self, name):
565            itemXML = ET.Element('{%s}%s' % (self.namespace, name))
566            self.xml.append(itemXML)
567
568
569    class UserStore(object):
570        def __init__(self):
571            self.users = {}
572
573        def __getitem__(self, jid):
574            return self.users.get(jid, None)
575
576        def register(self, jid, registration):
577            username = registration['username']
578
579            def filter_usernames(user):
580                return user != jid and self.users[user]['username'] == username
581
582            conflicts = filter(filter_usernames, self.users.keys())
583            if conflicts:
584                return False
585
586            self.users[jid] = registration
587            return True
588
589        def unregister(self, jid):
590            del self.users[jid]
591
592    class xep_0077(BasePlugin):
593        """
594        XEP-0077 In-Band Registration
595        """
596
597        def plugin_init(self):
598            self.description = "In-Band Registration"
599            self.xep = "0077"
600            self.form_fields = ('username', 'password')
601            self.form_instructions = ""
602            self.backend = UserStore()
603
604            self.xmpp.register_handler(
605                Callback('In-Band Registration',
606                         MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns),
607                         self.__handleRegistration))
608            register_stanza_plugin(Iq, Registration)
609
610        def post_init(self):
611            BasePlugin.post_init(self)
612            self.xmpp['xep_0030'].add_feature("jabber:iq:register")
613
614        def __handleRegistration(self, iq):
615            if iq['type'] == 'get':
616                # Registration form requested
617                userData = self.backend[iq['from'].bare]
618                self.sendRegistrationForm(iq, userData)
619            elif iq['type'] == 'set':
620                if iq['register']['remove']:
621                    # Remove an account
622                    self.backend.unregister(iq['from'].bare)
623                    self.xmpp.event('unregistered_user', iq)
624                    iq.reply().send()
625                    return
626
627                for field in self.form_fields:
628                    if not iq['register'][field]:
629                        # Incomplete Registration
630                        self._sendError(iq, '406', 'modify', 'not-acceptable',
631                                        "Please fill in all fields.")
632                        return
633
634                if self.backend.register(iq['from'].bare, iq['register']):
635                    # Successful registration
636                    self.xmpp.event('registered_user', iq)
637                    reply = iq.reply()
638                    reply.set_payload(iq['register'].xml)
639                    reply.send()
640                else:
641                    # Conflicting registration
642                    self._sendError(iq, '409', 'cancel', 'conflict',
643                                    "That username is already taken.")
644
645        def setForm(self, *fields):
646            self.form_fields = fields
647
648        def setInstructions(self, instructions):
649            self.form_instructions = instructions
650
651        def sendRegistrationForm(self, iq, userData=None):
652            reg = iq['register']
653            if userData is None:
654                userData = {}
655            else:
656                reg['registered'] = True
657
658            if self.form_instructions:
659                reg['instructions'] = self.form_instructions
660
661            for field in self.form_fields:
662                data = userData.get(field, '')
663                if data:
664                    # Add field with existing data
665                    reg[field] = data
666                else:
667                    # Add a blank field
668                    reg.addField(field)
669
670            reply = iq.reply()
671            reply.set_payload(reg.xml)
672            reply.send()
673
674        def _sendError(self, iq, code, error_type, name, text=''):
675            reply = iq.reply()
676            reply.set_payload(iq['register'].xml)
677            reply.error()
678            reply['error']['code'] = code
679            reply['error']['type'] = error_type
680            reply['error']['condition'] = name
681            reply['error']['text'] = text
682            reply.send()
683