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