summaryrefslogtreecommitdiff
path: root/docs/howto/stanzas.rst
blob: 56dfce79379740cd50877804e8dd983833a7cf0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
.. _work-with-stanzas:

How to Work with Stanza Objects
===============================

Slixmpp provides a large variety of facilities for abstracting the underlying
XML payloads of XMPP. Most of the visible user interface comes in a
dict-like interface provided in a specific ``__getitem__`` implementation
for :class:`~slixmpp.xmlstream.ElementBase` objects.


As a very high-level example, here is how to create a stanza with
an XEP-0191 payload, assuming the :class:`xep_0191 <slixmpp.plugins.xep_0191.XEP_0191>`
plugin is loaded:

.. code-block:: python

    from slixmpp.stanza import Iq
    iq = Iq()
    iq['to'] = 'toto@example.com'
    iq['type'] = 'set'
    iq['block']['items'] = {'a@example.com', 'b@example.com'}

Printing the resulting :class:`~slixmpp.stanaz.Iq` object gives us the
following XML (reformatted for readability):

.. code-block:: xml

    <iq xmlns="jabber:client" id="0" to="toto@example.com" type="set">
        <block xmlns="urn:xmpp:blocking">
            <item jid="b@example.com" />
            <item jid="a@example.com" />
        </block>
    </iq>


Realistically, users of the Slixmpp library should make use of the shorthand
functions available in their :class:`~.ClientXMPP` or
:class:`~.ComponentXMPP` objects to create :class:`~.Iq`, :class:`~.Message`
or :class:`~.Presence` objects that are bound to a stream, and which have
a generated unique identifier.

The most relevant functions are:

.. autofunction:: slixmpp.BaseXMPP.make_iq_get

.. autofunction:: slixmpp.BaseXMPP.make_iq_set

.. autofunction:: slixmpp.BaseXMPP.make_message

.. autofunction:: slixmpp.BaseXMPP.make_presence

The previous example then becomes:

.. code-block:: python

    iq = xmpp.make_iq_get(ito='toto@example.com')
    iq['block']['items'] = {'a@example.com', 'b@example.com'}


.. note::

    xml:lang is handled by piping the lang name after the attribute. For
    example ``message['body|fr']`` will return the ``<body/>`` attribute
    with ``xml:lang="fr``.

The next sections will try to explain as clearly as possible
how the magic operates.

.. _create-stanza-interfaces:

Defining Stanza Interfaces
--------------------------

The stanza interface is very rich and let developers have full control
over the API they want to have to manipulate stanzas.

The entire interface is defined as class attributes that are redefined
when subclassing :class:`~.ElementBase` when `creating a stanza plugin <create-stanza-plugins>`_.


The main attributes defining a stanza interface:

- plugin_attrib_: ``str``, the name of this element on the parent
- plugin_multi_attrib_: ``str``, the name of the iterable for this element on the parent
- interfaces_: ``set``, all known interfaces for this element
- sub_interfaces_: ``set`` (subset of ``interfaces``), for sub-elements with only text nodes
- bool_interfaces_: ``set`` (subset of ``interfaces``), for empty-sub-elements
- overrides_: ``list`` (subset of ``interfaces``), for ``interfaces`` to ovverride on the parent
- is_extension_: ``bool``, if the element is only an extension of the parent stanza

.. _plugin_attrib:

plugin_attrib
~~~~~~~~~~~~~

The ``plugin_attrib`` string is the defining element of any stanza plugin,
as it the name through which the element is accessed (except for ``overrides``
and ``is_extension``).

The extension is then registered through the help of :func:`~.register_stanza_plugin`
which will attach the plugin to its parent.

.. code-block:: python

    from slixmpp import ElementBase, Iq

    class Payload(ElementBase):
        name = 'apayload'
        plugin_attrib = 'mypayload'
        namespace = 'x-toto'

    register_stanza_plugin(Iq, Payload)

    iq = Iq()
    iq.enable('mypayload') # Similar to iq['mypayload']

The :class:`~.Iq` element created now contains our custom ``<apayload/>`` element.

.. code-block:: xml

    <iq xmlns="jabber:client" id="0">
        <apayload xmlns="x-toto"/>
    </iq>


.. _plugin_multi_attrib:

plugin_multi_attrib
~~~~~~~~~~~~~~~~~~~

The :func:`~.register_stanza_plugin` function has an ``iterable`` parameter, which
defaults to ``False``. When set to ``True``, it means that iterating over the element
is possible.


.. code-block:: python

    class Parent(ElementBase):
        pass # does not matter

    class Sub(ElementBase):
        name = 'sub'
        plugin_attrib = 'sub'

    class Sub2(ElementBase):
        name = 'sub2'
        plugin_attrib = 'sub2'

    register_stanza_plugin(Parent, Sub, iterable=True)
    register_stanza_plugin(Parent, Sub2, iterable=True)

    parent = Parent()
    parent.append(Sub())
    parent.append(Sub2())
    parent.append(Sub2())
    parent.append(Sub())

    for element in parent:
        do_something # A mix of Sub and Sub2 elements

In this situation, iterating over ``parent`` will yield each of the appended elements,
one after the other.

Sometimes you only want one specific type of sub-element, which is the use of
the ``plugin_multi_attrib`` string interface. This name will be mapped on the
parent, just like ``plugin_attrib``, but will return a list of all elements
of the same type only.

Re-using our previous example:

.. code-block:: python

    class Parent(ElementBase):
        pass # does not matter

    class Sub(ElementBase):
        name = 'sub'
        plugin_attrib = 'sub'
        plugin_multi_attrib = 'subs'

    class Sub2(ElementBase):
        name = 'sub2'
        plugin_attrib = 'sub2'
        plugin_multi_attrib = 'subs2'

    register_stanza_plugin(Parent, Sub, iterable=True)
    register_stanza_plugin(Parent, Sub2, iterable=True)

    parent = Parent()
    parent.append(Sub())
    parent.append(Sub2())
    parent.append(Sub2())
    parent.append(Sub())

    for sub in parent['subs']:
        do_something # ony Sub objects here

    for sub2 in parent['subs2']:
        do_something # ony Sub2 objects here


.. _interfaces:

interfaces
~~~~~~~~~~

The ``interfaces`` set **must** contain all the known ways to interact with
this element. It does not include plugins (registered to the element through
:func:`~.register_stanza_plugin`), which are dynamic.

By default, a name present in ``interfaces`` will be mapped to an attribute
of the element with the same name.

.. code-block:: python

    class Example(Element):
        name = 'example'
        interfaces = {'toto'}

    example = Example()
    example['toto'] = 'titi'

In this case, ``example`` contains ``<example toto="titi"/>``.

For empty and text_only sub-elements, there are sub_interfaces_ and
bool_interfaces_ (the keys **must** still be in ``interfaces``.

You can however define any getter, setter, and delete custom method for any of
those interfaces. Keep in mind that if one of the three is not custom,
Slixmpp will use the default one, so you have to make sure that either you
redefine all get/set/del custom methods, or that your custom methods are
compatible with the default ones.

In the following example, we want the ``toto`` attribute to be an integer.

.. code-block:: python

    class Example(Element):
        interfaces = {'toto', 'titi', 'tata'}

        def get_toto(self) -> Optional[int]:
            try:
                return int(self.xml.attrib.get('toto', ''))
            except ValueError:
                return None

        def set_toto(self, value: int):
            int(value) # make sure the value is an int
            self.xml.attrib['toto'] = str(value)

        example = Example()
        example['tata'] = "Test" # works
        example['toto'] = 1 # works
        print(type(example['toto'])) # the value is an int
        example['toto'] = "Test 2" # ValueError


One important thing to keep in mind is that the ``get_`` methods must be resilient
(when having a default value makes sense) because they are called on objects
received from the network.

.. _sub_interfaces:

sub_interfaces
~~~~~~~~~~~~~~

The ``bool_interfaces`` set allows mapping an interface to the text node of
sub-element of the current payload, with the same namespace

Here is a simple example:

.. code-block:: python

    class FirstLevel(ElementBase):
        name = 'first'
        namespace = 'ns'
        interfaces = {'second'}
        sub_interfaces = {'second'}

    parent = FirstLevel()
    parent['second'] = 'Content of second node'


Which will produces the following:

.. code-block:: xml

    <first xmlns="ns">
        <second>Content of second node</second>
    </first>

We can see that ``sub_interfaces`` allows to quickly create a sub-element and
manipulate its text node without requiring a custom element, getter or setter.

.. _bool_interfaces:

bool_interfaces
~~~~~~~~~~~~~~~

The ``bool_interfaces`` set allows mapping an interface to a direct sub-element of the
current payload, with the same namespace.


Here is a simple example:

.. code-block:: python

    class FirstLevel(ElementBase):
        name = 'first'
        namespace = 'ns'
        interfaces = {'second'}
        bool_interfaces = {'second'}

    parent = FirstLevel()
    parent['second'] = True


Which will produces the following:

.. code-block:: xml

    <first xmlns="ns">
        <second/>
    </first>

We can see that ``bool_interfaces`` allows to quickly create sub-elements with no
content, without the need to create a custom class or getter/setter.

overrides
~~~~~~~~~

List of ``interfaces`` on the present element that should override the
parent ``interfaces`` with the same name.

.. code-block:: python

    class Parent(ElementBase):
        name = 'parent'
        interfaces = {'toto', 'titi'}

    class Sub(ElementBase):
        name = 'sub'
        plugin_attrib = name
        interfaces = {'toto', 'titi'}
        overrides = ['toto']

    register_stanza_plugin(Parent, Sub)

    parent = Parent()
    parent['toto'] = 'test' # equivalent to parent['sub']['toto'] = "test"

is_extension
~~~~~~~~~~~~

Stanza extensions are a specific kind of stanza plugin which have
the ``is_extension`` class attribute set to ``True``.

The following code will directly plug the extension into the
:class:`~.Message` element, allowing direct access
to the interface:

.. code-block:: python

    class MyCustomExtension(ElementBase):
        is_extension = True
        name = 'mycustom'
        namespace = 'custom-ns'
        plugin_attrib = 'mycustom'
        interfaces = {'mycustom'}

    register_stanza_plugin(Message, MyCustomExtension)

With this extension, we can do the folliowing:

.. code-block:: python

    message = Message()
    message['mycustom'] = 'toto'

Without the extension, obtaining the same results would be:

.. code-block:: python

    message = Message()
    message['mycustom']['mycustom'] = 'toto'


The extension is therefore named extension because it extends the
parent element transparently.


.. _create-stanza-plugins:

Creating Stanza Plugins
-----------------------

A stanza plugin is a class that inherits from :class:`~.ElementBase`, and
**must** contain at least the following attributes:

- name: XML element name (e.g. ``toto`` if the element is ``<toto/>``
- namespace: The XML namespace of the element.
- plugin_attrib_: ``str``, the name of this element on the parent
- interfaces_: ``set``, all known interfaces for this element

It is then registered through :func:`~.register_stanza_plugin` on the parent
element.

.. note::

    :func:`~.register_stanza_plugin` should NOT be called at the module level,
    because it executes code, and executing code at the module level can slow
    down import significantly!