summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/pull_request_template.md13
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--.travis.yml6
-rw-r--r--INSTALL2
-rw-r--r--README.rst2
-rw-r--r--docs/_static/haiku.css21
-rw-r--r--docs/_templates/layout.html1
-rw-r--r--docs/api/xmlstream/tostring.rst2
-rw-r--r--docs/differences.rst5
-rw-r--r--docs/getting_started/sendlogout.rst14
-rw-r--r--docs/howto/make_plugin_extension_for_message_and_iq.pl.rst1799
-rw-r--r--docs/howto/make_plugin_extension_for_message_and_iq.rst1796
-rw-r--r--docs/index.rst2
-rwxr-xr-xexamples/adhoc_provider.py4
-rwxr-xr-xexamples/adhoc_user.py6
-rwxr-xr-xexamples/admin_commands.py4
-rwxr-xr-xexamples/disco_browser.py2
-rwxr-xr-xexamples/download_avatars.py2
-rwxr-xr-xexamples/echo_client.py4
-rwxr-xr-xexamples/gtalk_custom_domain.py4
-rwxr-xr-xexamples/markup.py4
-rwxr-xr-xexamples/migrate_roster.py2
-rwxr-xr-xexamples/muc.py4
-rwxr-xr-xexamples/ping.py4
-rwxr-xr-xexamples/proxy_echo_client.py4
-rwxr-xr-xexamples/pubsub_client.py2
-rwxr-xr-xexamples/pubsub_events.py4
-rwxr-xr-xexamples/register_account.py4
-rwxr-xr-xexamples/roster_browser.py8
-rwxr-xr-xexamples/send_client.py6
-rwxr-xr-xexamples/set_avatar.py4
-rwxr-xr-xexamples/thirdparty_auth.py4
-rwxr-xr-xexamples/user_location.py4
-rwxr-xr-xexamples/user_tune.py4
-rwxr-xr-xsetup.py6
-rw-r--r--slixmpp/basexmpp.py3
-rw-r--r--slixmpp/jid.py5
-rw-r--r--slixmpp/plugins/__init__.py3
-rw-r--r--slixmpp/plugins/xep_0009/stanza/RPC.py2
-rw-r--r--slixmpp/plugins/xep_0030/disco.py2
-rw-r--r--slixmpp/plugins/xep_0045.py422
-rw-r--r--slixmpp/plugins/xep_0045/__init__.py14
-rw-r--r--slixmpp/plugins/xep_0045/muc.py382
-rw-r--r--slixmpp/plugins/xep_0045/stanza.py198
-rw-r--r--slixmpp/plugins/xep_0048/stanza.py7
-rw-r--r--slixmpp/plugins/xep_0059/rsm.py7
-rw-r--r--slixmpp/plugins/xep_0115/caps.py7
-rw-r--r--slixmpp/plugins/xep_0196/stanza.py3
-rw-r--r--slixmpp/plugins/xep_0198/stream_management.py46
-rw-r--r--slixmpp/plugins/xep_0202/time.py2
-rw-r--r--slixmpp/plugins/xep_0279/ipcheck.py4
-rw-r--r--slixmpp/plugins/xep_0313/mam.py78
-rw-r--r--slixmpp/plugins/xep_0323/stanza/sensordata.py2
-rw-r--r--slixmpp/plugins/xep_0332/http.py3
-rw-r--r--slixmpp/plugins/xep_0363/http_upload.py9
-rw-r--r--slixmpp/plugins/xep_0377/__init__.py15
-rw-r--r--slixmpp/plugins/xep_0377/spam_reporting.py41
-rw-r--r--slixmpp/plugins/xep_0377/stanza.py70
-rw-r--r--slixmpp/plugins/xep_0421/__init__.py13
-rw-r--r--slixmpp/plugins/xep_0421/occupant_id.py32
-rw-r--r--slixmpp/plugins/xep_0421/stanza.py41
-rw-r--r--slixmpp/plugins/xep_0444/__init__.py11
-rw-r--r--slixmpp/plugins/xep_0444/reactions.py63
-rw-r--r--slixmpp/plugins/xep_0444/stanza.py60
-rw-r--r--slixmpp/stanza/message.py44
-rw-r--r--slixmpp/stanza/presence.py4
-rw-r--r--slixmpp/test/slixtest.py17
-rw-r--r--slixmpp/thirdparty/mini_dateutil.py2
-rw-r--r--slixmpp/version.py4
-rw-r--r--slixmpp/xmlstream/stanzabase.py30
-rw-r--r--slixmpp/xmlstream/xmlstream.py198
-rw-r--r--tests/test_stanza_base.py8
-rw-r--r--tests/test_stanza_xep_0377.py56
-rw-r--r--tests/test_stanza_xep_0421.py29
-rw-r--r--tests/test_stanza_xep_0444.py69
-rw-r--r--tests/test_stream_xep_0323.py2
76 files changed, 5133 insertions, 631 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..50ea54cb
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,13 @@
+################ Please use Gitlab instead of Github ###################################
+
+Hello, thank you for contributing to slixmpp!
+
+You’re about to open a pull request on github. However this github repository is not the official place for contributions on slixmpp.
+
+Please open your merge request on https://lab.louiz.org/poezio/slixmpp/
+
+You should be able to log in there with your github credentials, clone the slixmpp repository in your namespace, push your existing pull request into a new branch, and then open a merge request with one click, within 3 minutes.
+
+This will help us review your contribution, avoid spreading things everywhere and it will even run the tests automatically with your changes.
+
+Thank you.
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 839de025..0a4013b6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,8 @@ test:
image: ubuntu:latest
script:
- apt update
- - apt install -y python3 cython3 gpg
+ - apt install -y python3 python3-pip cython3 gpg
+ - pip3 install emoji
- ./run_tests.py
trigger_poezio:
diff --git a/.travis.yml b/.travis.yml
index f503be34..be8e089c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,7 @@
language: python
python:
- - "3.4"
- - "3.5"
- - "3.6"
- - "3.7-dev"
+ - "3.7"
+ - "3.8-dev"
install:
- "pip install ."
script: testall.py
diff --git a/INSTALL b/INSTALL
index 9b14b232..146031f4 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,5 +1,5 @@
Pre-requisites:
-- Python 3.5+
+- Python 3.7+
- Cython 0.22 and libidn, optionally (making JID faster by compiling the stringprep module)
- GnuPG, for testing
diff --git a/README.rst b/README.rst
index d27f0698..6a25248d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
Slixmpp
#########
-Slixmpp is an MIT licensed XMPP library for Python 3.5+. It is a fork of
+Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of
SleekXMPP.
Slixmpp's goals is to only rewrite the core of the library (the low level
diff --git a/docs/_static/haiku.css b/docs/_static/haiku.css
index 3d8ee6a7..a76f55de 100644
--- a/docs/_static/haiku.css
+++ b/docs/_static/haiku.css
@@ -408,24 +408,3 @@ div.viewcode-block:target {
margin: -1px -12px;
padding: 0 12px;
}
-
-#from_andyet {
- -webkit-box-shadow: #CCC 0px 0px 3px;
- background: rgba(255, 255, 255, 1);
- bottom: 0px;
- right: 17px;
- padding: 3px 10px;
- position: fixed;
-}
-
-#from_andyet h2 {
- background-image: url("images/from_&yet.png");
- background-repeat: no-repeat;
- height: 29px;
- line-height: 0;
- text-indent: -9999em;
- width: 79px;
- margin-top: 0;
- margin: 0px;
- padding: 0px;
-}
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index de6f7244..0a97cb70 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -65,6 +65,5 @@
<div class="bottomnav">
{{ nav() }}
</div>
- <a id="from_andyet" href="http://andyet.net"><h2>From &amp;yet</h2></a>
{% endblock %}
diff --git a/docs/api/xmlstream/tostring.rst b/docs/api/xmlstream/tostring.rst
index 68abbdb6..107e97b0 100644
--- a/docs/api/xmlstream/tostring.rst
+++ b/docs/api/xmlstream/tostring.rst
@@ -13,7 +13,7 @@ hides namespaces when able and does not introduce excessive namespace
prefixes::
>>> from slixmpp.xmlstream.tostring import tostring
- >>> from xml.etree import cElementTree as ET
+ >>> from xml.etree import ElementTree as ET
>>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>')
>>> ET.tostring(xml)
'<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>'
diff --git a/docs/differences.rst b/docs/differences.rst
index 8a86427b..7c781571 100644
--- a/docs/differences.rst
+++ b/docs/differences.rst
@@ -3,8 +3,9 @@
Differences from SleekXMPP
==========================
-**Python 3.5+ only**
- slixmpp will only work on python 3.5 and above.
+**Python 3.7+ only**
+ slixmpp will work on python 3.7 and above. It may work with previous
+ versions but we provide no guarantees.
**Stanza copies**
The same stanza object is given through all the handlers; a handler that
diff --git a/docs/getting_started/sendlogout.rst b/docs/getting_started/sendlogout.rst
index a27976c5..3b5d6d5a 100644
--- a/docs/getting_started/sendlogout.rst
+++ b/docs/getting_started/sendlogout.rst
@@ -47,11 +47,11 @@ the roster. Next, we want to send our message, and to do that we will use :meth:
self.send_message(mto=self.recipient, mbody=self.msg)
Finally, we need to disconnect the client using :meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>`.
-Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call
-:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` without any parameters, then it is possible
-for the client to disconnect before the send queue is processed and the message is actually
-sent on the wire. To ensure that our message is processed, we use
-:meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>`.
+Now, sent stanzas are placed in a queue to pass them to the send thread.
+:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` by default will wait for an
+acknowledgement from the server for at least `2.0` seconds. This time is configurable with
+the `wait` parameter. If `0.0` is passed for `wait`, :meth:`disconnect
+<slixmpp.xmlstream.XMLStream.disconnect>` will not close the connection gracefully.
.. code-block:: python
@@ -61,12 +61,12 @@ sent on the wire. To ensure that our message is processed, we use
self.send_message(mto=self.recipient, mbody=self.msg)
- self.disconnect(wait=True)
+ self.disconnect()
.. warning::
If you happen to be adding stanzas to the send queue faster than the send thread
- can process them, then :meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>`
+ can process them, then :meth:`disconnect() <slixmpp.xmlstream.XMLStream.disconnect>`
will block and not disconnect.
Final Product
diff --git a/docs/howto/make_plugin_extension_for_message_and_iq.pl.rst b/docs/howto/make_plugin_extension_for_message_and_iq.pl.rst
new file mode 100644
index 00000000..fb00cbf7
--- /dev/null
+++ b/docs/howto/make_plugin_extension_for_message_and_iq.pl.rst
@@ -0,0 +1,1799 @@
+Jak stworzyć własny plugin rozszerzający obiekty Message i Iq w Slixmpp
+========================================================================
+
+Wstęp i wymagania
+------------------
+
+* `'python3'`
+
+Kod użyty w tutorialu jest kompatybilny z pythonem w wersji 3.6 lub nowszej.
+Dla uzyskania kompatybilności z wcześniejszymi wersjami należy zastąpić f-strings starszym formatowaniem napisów `'"{}".format("content")'` lub `'%s, "content"'`.
+
+Instalacja dla Ubuntu linux:
+
+.. code-block:: bash
+
+ sudo apt-get install python3.6
+
+* `'slixmpp'`
+* `'argparse'`
+* `'logging'`
+* `'subprocess'`
+
+Wszystkie biblioteki wymienione powyżej, za wyjątkiem slixmpp, należą do standardowej biblioteki pythona. Zdarza się, że kompilując źródła samodzielnie, część z nich może nie zostać zainstalowana.
+
+.. code-block:: python
+
+ python3 --version
+ python3 -c "import slixmpp; print(slixmpp.__version__)"
+ python3 -c "import argparse; print(argparse.__version__)"
+ python3 -c "import logging; print(logging.__version__)"
+ python3 -m subprocess
+
+Wynik w terminalu:
+
+.. code-block:: bash
+
+ ~ $ python3 --version
+ Python 3.8.0
+ ~ $ python3 -c "import slixmpp; print(slixmpp.__version__)"
+ 1.4.2
+ ~ $ python3 -c "import argparse; print(argparse.__version__)"
+ 1.1
+ ~ $ python3 -c "import logging; print(logging.__version__)"
+ 0.5.1.2
+ ~ $ python3 -m subprocess # Nie powinno nic zwrócić
+
+Jeśli któraś z bibliotek zwróci `'ImportError'` lub `'no module named ...'`, należy je zainstalować zgodnie z przykładem poniżej:
+
+Instalacja Ubuntu linux:
+
+.. code-block:: bash
+
+ pip3 install slixmpp
+ #or
+ easy_install slixmpp
+
+Jeśli jakaś biblioteka zwróci NameError, należy zainstalować pakiet ponownie.
+
+* `Konta dla Jabber`
+
+Do testowania niezbędne będą dwa prywatne konta jabbera. Można je stworzyć na jednym z dostępnych darmowych serwerów:
+https://www.google.com/search?q=jabber+server+list
+
+Skrypt uruchamiający klientów
+------------------------------
+
+Skrypt pozwalający testować klientów powinien zostać stworzony poza lokalizacją projektu. Pozwoli to szybko sprawdzać wyniki skryptów oraz uniemożliwi przypadkowe wysłanie swoich danych na gita.
+
+Przykładowo, można stworzyć plik o nazwie `'test_slixmpp'` w lokalizacji `'/usr/bin'` i nadać mu uprawnienia wykonawcze:
+
+.. code-block:: bash
+
+ /usr/bin $ chmod 711 test_slixmpp
+
+Plik zawiera prostą strukturę, która pozwoli nam zapisać dane logowania.
+
+.. code-block:: python
+
+#!/usr/bin/python3
+#File: /usr/bin/test_slixmpp & permissions rwx--x--x (711)
+
+import subprocess
+import time
+
+if __name__ == "__main__":
+ #~ prefix = ["x-terminal-emulator", "-e"] # Osobny terminal dla kazdego klienta, może być zastąpiony inną konsolą.
+ #~ prefix = ["xterm", "-e"]
+ prefix = []
+ #~ suffix = ["-d"] # Debug
+ #~ suffix = ["-q"] # Quiet
+ suffix = []
+
+ sender_path = "./example/sender.py"
+ sender_jid = "SENDER_JID"
+ sender_password = "SENDER_PASSWORD"
+
+ example_file = "./test_example_tag.xml"
+
+ responder_path = "./example/responder.py"
+ responder_jid = "RESPONDER_JID"
+ responder_password = "RESPONDER_PASSWORD"
+
+ # Remember about the executable permission. (`chmod +x ./file.py`)
+ SENDER_TEST = prefix + [sender_path, "-j", sender_jid, "-p", sender_password, "-t", responder_jid, "--path", example_file] + suffix
+ RESPON_TEST = prefix + [responder_path, "-j", responder_jid, "-p", responder_password] + suffix
+
+ try:
+ responder = subprocess.Popen(RESPON_TEST)
+ sender = subprocess.Popen(SENDER_TEST)
+ responder.wait()
+ sender.wait()
+ except:
+ try:
+ responder.terminate()
+ except NameError:
+ pass
+ try:
+ sender.terminate()
+ except NameError:
+ pass
+ raise
+
+Skrypt uruchamiający powinien być dostosowany do potrzeb urzytkownika: można w nim pobierać ścieżki do projektu z linii komend (przez `'sys.argv[...]'` lub `'os.getcwd()'`), wybierać z jaką flagą mają zostać uruchomione programy oraz wiele innych. Jego należyte przygotowanie pozwoli zaoszczędzić czas i nerwy podczas późniejszych prac.
+
+W przypadku testowania większych aplikacji, w tworzeniu pluginu szczególnie użyteczne jest nadanie unikalnych nazwy dla każdego klienta (w konsekwencji: różne linie poleceń). Pozwala to szybko określić, który klient co zwraca, bądź który powoduje błąd.
+
+Stworzenie klienta i pluginu
+-----------------------------
+
+W stosownej dla nas lokalizacji powinniśmy stworzyć dwa klienty slixmpp (w przykładach: `'sender'` i `'responder'`), aby sprawdzić czy skrypt uruchamiający działa poprawnie. Poniżej przedstawiona została minimalna niezbędna implementacja, która może testować plugin w trakcie jego projektowania:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online.
+ self.send_presence()
+ self.get_roster()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ #xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin jest nazwą klasy example_plugin.
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+
+ import slixmpp
+ import example_plugin
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online
+ self.send_presence()
+ self.get_roster()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Responder.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message to")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Responder(args.jid, args.password)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin jest nazwą klasy example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+Następny plik, który należy stworzyć to `'example_plugin'`. Powinien być w lokalizacji dostępnej dla klientów (domyślnie w tej samej, co skrypty klientów).
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ Napis czytelny dla człowieka i dla znalezienia pluginu przez inny plugin
+ self.xep = "ope" ##~ Napis czytelny dla człowieka i dla znalezienia pluginu przez inny plugin poprzez dodanie tego do `slixmpp/plugins/__init__.py`, w polu `__all__` z prefixem xep 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ Nazwa głównego pliku XML w tym rozszerzeniu.
+ namespace = "https://example.net/our_extension" ##~ Namespace obiektu jest definiowana w tym miejscu, powinien się odnosić do nazwy portalu xmpp; w wiadomości wygląda tak: <example_tag xmlns={namespace} (...)</example_tag>
+
+ plugin_attrib = "example_tag" ##~ Nazwa pod którą można odwoływać się do danych zawartych w tym pluginie. Bardziej szczegółowo: tutaj rejestrujemy nazwę obiektu by móc się do niego odwoływać z zewnątrz. Można się do niego odwoływać jak do słownika: stanza_object['example_tag'], gdzie `'example_tag'` jest nazwą pluginu i powinno być takie samo jak name.
+
+ interfaces = {"boolean", "some_string"} ##~ Zbiór kluczy dla słownika atrybutów elementu które mogą być użyte w elemencie. Na przykład `stanza_object['example_tag']` poda informacje o: {"boolean": "some", "some_string": "some"}, tam gdzie `'example_tag'` jest elementu.
+
+Jeżeli powyższy plugin nie jest w domyślnej lokalizacji, a klienci powinni pozostać poza repozytorium, możemy w miejscu klientów dodać dowiązanie symboliczne do lokalizacji pluginu:
+
+.. code-block:: bash
+
+ ln -s $Path_to_example_plugin_py $Path_to_clients_destinations
+
+Jeszcze innym wyjściem jest import relatywny z użyciem kropek '.' aby dostać się do właściwej ścieżki.
+
+Pierwsze uruchomienie i przechwytywanie zdarzeń
+-------------------------------------------------
+
+Aby sprawdzić czy wszystko działa prawidłowo, można użyć metody `'start'`. Jest jej przypisane zdarzenie `'session_start'`. Sygnał ten zostanie wysłany w momencie, w którym klient będzie gotów do działania. Stworzenie własnej metoda pozwoli na zdefiniowanie działania tego sygnału.
+
+W metodzie `'__init__'` zostało stworzone przekierowanie zdarzenia `'session_start'`. Kiedy zostanie on wywołany, metoda `'def start(self, event):'` zostanie wykonana. Jako pierwszy krok procesie tworzenia, można dodać linię `'logging.info("I'm running")'` w obu klientach (sender i responder), a następnie użyć komendy `'test_slixmpp'`.
+
+Metoda `'def start(self, event):'` powinna wyglądać tak:
+
+.. code-block:: python
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ logging.info("I'm running")
+ #<<<<<<<<<<<<
+
+Jeżeli oba klienty uruchomiły się poprawnie, można zakomentować tą linię.
+
+Budowanie obiektu Message
+-------------------------
+
+Wysyłający powinien posiadać informację o tym, do kogo należy wysłać wiadomość. Nazwę i ścieżkę odbiorcy można przekazać, na przykład, przez argumenty wywołania skryptu w linii komend. W poniższym przykładzie, są one trzymane w atrybucie `'self.to'`.
+
+Przykład:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+ #>>>>>>>>>>>>
+ self.send_example_message(self.to, "example_message")
+
+ def send_example_message(self, to, body):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Domyślnie mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ msg.send()
+ #<<<<<<<<<<<<
+
+W przykładzie powyżej, używana jest wbudowana metoda `'make_message'`, która tworzy wiadomość o treści `'example_message'` i wysyła ją pod koniec działania metody start. Czyli: wiadomość ta zostanie wysłana raz, zaraz po uruchomieniu skryptu.
+
+Aby otrzymać tę wiadomość, responder powinien wykorzystać odpowiednie zdarzenie: metodę, która określa co zrobić, gdy zostanie odebrana wiadomość której nie zostało przypisane żadne inne zdarzenie. Przykład takiego kodu:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+
+ #>>>>>>>>>>>>
+ self.add_event_handler("message", self.message)
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ def message(self, msg):
+ #Pokazuje cały XML wiadomości
+ logging.info(msg)
+ #Pokazuje wyłącznie pole 'body' wiadomości
+ logging.info(msg['body'])
+ #<<<<<<<<<<<<
+
+Rozszerzenie Message o nowy tag
+--------------------------------
+
+Aby rozszerzyć obiekt Message o wybrany tag, plugin powinien zostać zarejestrowany jako rozszerzenie dla obiektu Message:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin.
+ self.xep = "ope" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin przez dodanie go do `slixmpp/plugins/__init__.py` w metodzie `__all__` z 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ #>>>>>>>>>>>>
+ register_stanza_plugin(Message, ExampleTag) ##~ Zarejestrowany rozszerzony tag dla obiektu Message. Jeśli to nie zostanie zrobione, message['example_tag'] będzie polem tekstowym, a nie rozszerzeniem i nie będzie mogło zawierać atrybutów i pod-elementów.
+ #<<<<<<<<<<<<
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ Nazwa głównego pliku XML dla tego rozszerzenia..
+ namespace = "https://example.net/our_extension" ##~ Nazwa obiektu, np. <example_tag xmlns={namespace} (...)</example_tag>. Powinna zostać zmieniona na własną.
+
+ plugin_attrib = "example_tag" ##~ Nazwa, którą można odwołać się do obiektu. W szczególności, do zarejestrowanego obiektu można odwołać się przez: nazwa_obiektu['tag']. gdzie `'tag'` jest nazwą ElementBase extension. Nazwa powinna być taka sama jak "name" wyżej.
+
+ interfaces = {"boolean", "some_string"} ##~ Lista kluczy słownika, które mogą być użyte z obiektem. Na przykład: `stanza_object['example_tag']` zwraca {"another": "some", "data": "some"}, gdzie `'example_tag'` jest nazwą rozszerzenia ElementBase.
+
+ #>>>>>>>>>>>>
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+ #<<<<<<<<<<<<
+
+Teraz, po rejestracji tagu, można rozszerzyć wiadomość.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+ self.send_example_message(self.to, "example_message")
+
+ def send_example_message(self, to, body):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Default mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ #>>>>>>>>>>>>
+ msg['example_tag']['some_string'] = "Work!"
+ logging.info(msg)
+ #<<<<<<<<<<<<
+ msg.send()
+
+Po uruchomieniu, obiekt logging powinien wyświetlić Message wraz z tagiem `'example_tag'` zawartym w środku <message><example_tag/></message>, oraz z napisem `'Work'` i nadaną przestrzenią nazw.
+
+Nadanie oddzielnego sygnału dla rozszerzonej wiadomości
+--------------------------------------------------------
+
+Jeśli zdarzenie nie zostanie sprecyzowane, to zarówno rozszerzona jak i podstawowa wiadomość będą przechwytywane przez sygnał `'message'`. Aby nadać im oddzielne zdarzenie, należy zarejestrować odpowiedni uchwyt dla przestrzeni nazw i tagu, aby stworzyć unikalną kombinację, która pozwoli na przechwycenie wyłącznie pożądanych wiadomości (lub Iq object).
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin.
+ self.xep = "ope" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin przez dodanie go do `slixmpp/plugins/__init__.py` w metodzie `__all__` z 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Nazwa tego Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Przechwytuje wyłącznie Message z tagiem example_tag i przestrzenią nazw taką, jaką zdefiniowaliśmy w ExampleTag
+ self.__handle_message)) ##~ Metoda do której zostaje przypisany przechwycony odpowiedni obiekt, powinna wywołać odpowiedni dla klienta wydarzenie.
+ register_stanza_plugin(Message, ExampleTag) ##~ Zarejestrowany rozszerzony tag dla obiektu Message. Jeśli to nie zostanie zrobione, message['example_tag'] będzie polem tekstowym, a nie rozszerzeniem i nie będzie mogło zawierać atrybutów i pod-elementów.
+
+ def __handle_message(self, msg):
+ # Tu można coś zrobić z przechwyconą wiadomością zanim trafi do klienta.
+ self.xmpp.event('example_tag_message', msg) ##~ Wywołuje zdarzenie, które może zostać przechwycone i obsłużone przez klienta, jako argument przekazujemy obiekt który chcemy dopiąć do wydarzenia.
+
+Obiekt StanzaPath powinien być poprawnie zainicjalizowany, według schematu:
+`'NAZWA_OBIEKTU[@type=TYP_OBIEKTU][/{NAMESPACE}[TAG]]'`
+
+* Dla NAZWA_OBIEKTU można użyć `'message'` lub `'iq'`.
+* Dla TYP_OBIEKTU, jeśli obiektem jest iq, można użyć typu spośród: `'get, set, error or result'`. Jeśli obiektem jest Message, można sprecyzować typ np. `'chat'`..
+* Dla NAMESPACE powinna to byc przestrzeń nazw zgodna z rozszerzeniem tagu.
+* TAG powinien zawierać tag, tutaj: `'example_tag'`.
+
+Teraz program przechwyci wszystkie wiadomości typu message, które zawierają sprecyzowaną przestrzeń nazw wewnątrz `'example_tag'`. Można też sprawdzić co Message zawiera, czy na pewno posiada wymagane pola itd. Następnie wiadomość jest wysyłana do klienta za pośrednictwem wydarzenia `'example_tag_message'`.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+ #>>>>>>>>>>>>
+ self.send_example_message(self.to, "example_message", "example_string")
+
+ def send_example_message(self, to, body, some_string=""):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Default mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ if some_string:
+ msg['example_tag'].set_some_string(some_string)
+ msg.send()
+ #<<<<<<<<<<<<
+
+Należy zapamiętać linię: `'self.xmpp.event('example_tag_message', msg)'`. W tej linii została zdefiniowana nazwa zdarzenia do przechwycenia wewnątrz pliku "responder.py". Tutaj to: `'example_tag_message'`.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_message", self.example_tag_message) # Rejestracja uchwytu
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Metody niewymagane, ale pozwalające na zobaczenie dostępności online.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ def example_tag_message(self, msg):
+ logging.info(msg) # Message jest obiektem który nie wymaga wiadomości zwrotnej, ale nic się nie stanie, gdy zostanie wysłana.
+ #<<<<<<<<<<<<
+
+Można odesłać wiadomość, ale nic się nie stanie jeśli to nie zostanie zrobione.
+Natomiast obiekt komunikacji (Iq) już będzie wymagał odpowiedzi, więc obydwaj klienci powinni pozostawać online. W innym wypadku, klient otrzyma automatyczny error z powodu timeoutu, jeśli cell Iq nie odpowie za pomocą Iq o tym samym Id.
+
+Użyteczne metody i inne
+------------------------
+
+Modyfikacja przykładowego obiektu `Message` na obiekt `Iq`
+----------------------------------------------------------
+
+Aby przerobić przykładowy obiekt Message na obiekt Iq, należy zarejestrować nowy uchwyt (handler) dla Iq, podobnie jak zostało to przedstawione w rozdziale `,,Rozszerzenie Message o tag''`. Tym razem, przykład będzie zawierał kilka rodzajów Iq o oddzielnych typami. Poprawia to czytelność kodu oraz usprawnia weryfikację poprawności działania. Wszystkie Iq powinny odesłać odpowiedź z tym samym Id i odpowiedzią do wysyłającego. W przeciwnym wypadku, wysyłający dostanie Iq zwrotne typu error.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin.
+ self.xep = "ope" ##~ Napis zrozumiały dla ludzi oraz do znalezienia pluginu przez inny plugin przez dodanie go do `slixmpp/plugins/__init__.py` w metodzie `__all__` z 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ #>>>>>>>>>>>>
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Nazwa tego Callbacka
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'get' oraz example_tag
+ self.__handle_get_iq)) ##~ Metoda obsługująca odpowiednie Iq, powinna wywołać zdarzenie dla klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleResult Event:example_tag', ##~ Nazwa tego Callbacka
+ StanzaPath(f"iq@type=result/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'result' oraz example_tag
+ self.__handle_result_iq)) ##~ Metoda obsługująca odpowiednie Iq, powinna wywołać zdarzenie dla klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleError Event:example_tag', ##~ Nazwa tego Callbacka
+ StanzaPath(f"iq@type=error/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'error' oraz example_tag
+ self.__handle_error_iq)) ##~ Metoda obsługująca odpowiednie Iq, powinna wywołać zdarzenie dla klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Nazwa tego Callbacka
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Obsługuje tylko Iq z example_tag
+ self.__handle_message)) ##~ Metoda obsługująca odpowiednie Iq, powinna wywołać zdarzenie dla klienta.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Rejestruje rozszerzenie taga dla obiektu Iq. W przeciwnym wypadku, Iq['example_tag'] będzie polem string zamiast kontenerem.
+ #<<<<<<<<<<<<
+ register_stanza_plugin(Message, ExampleTag) ##~ Rejestruje rozszerzenie taga dla obiektu Message. W przeciwnym wypadku, message['example_tag'] będzie polem string zamiast kontenerem.
+
+ #>>>>>>>>>>>>
+ # Wszystkie możliwe typy Iq to: get, set, error, result
+ def __handle_get_iq(self, iq):
+ # Zrób coś z otrzymanym iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Wywołuje zdarzenie, który może być obsłużony przez klienta lub inaczej.
+
+ def __handle_result_iq(self, iq):
+ # Zrób coś z otrzymanym Iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Wywołuje zdarzenie, który może być obsłużony przez klienta lub inaczej.
+
+ def __handle_error_iq(self, iq):
+ # Zrób coś z otrzymanym Iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Wywołuje zdarzenie, który może być obsłużony przez klienta lub inaczej.
+
+ def __handle_message(self, msg):
+ # Zrób coś z otrzymaną wiadomością
+ self.xmpp.event('example_tag_message', msg) ##~ Wywołuje zdarzenie, który może być obsłużony przez klienta lub inaczej.
+
+Wydarzenia wywołane przez powyższe uchwyty mogą zostać przechwycone tak, jak w przypadku wydarzenia `'example_tag_message'`.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_message", self.example_tag_message)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_get_iq", self.example_tag_get_iq)
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def example_tag_get_iq(self, iq): # Iq stanza powinno zawsze zostać zwrócone, w innym wypadku wysyłający dostanie informacje z błędem.
+ logging.info(str(iq))
+ reply = iq.reply(clear=False)
+ reply.send()
+ #<<<<<<<<<<<<
+
+Domyślnie parametr `'clear'` dla `'Iq.reply'` jest ustawiony na True. Wtedy to, co jest zawarte wewnątrz Iq (z kilkoma wyjątkami) powinno zostać zdefiniowane ponownie. Jedyne informacje które zostaną w Iq po metodzie reply, nawet gdy parametr clean jest ustawiony na True, to ID tego Iq oraz JID wysyłającego.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag']['boolean'] = "True"
+ iq['example_tag']['some_string'] = "Another_string"
+ iq['example_tag'].text = "Info_inside_tag"
+ iq.send()
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def example_tag_result_iq(self, iq):
+ logging.info(str(iq))
+
+ def example_tag_error_iq(self, iq):
+ logging.info(str(iq))
+ #<<<<<<<<<<<<
+
+Dostęp do elementów
+-------------------------
+
+Jest kilka możliwości dostania się do pól wewnątrz Message lub Iq. Po pierwsze, z poziomu klienta, można dostać zawartość jak ze słownika:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ #...
+ def example_tag_result_iq(self, iq):
+ logging.info(str(iq))
+ #>>>>>>>>>>>>
+ logging.info(iq['id'])
+ logging.info(iq.get('id'))
+ logging.info(iq['example_tag']['boolean'])
+ logging.info(iq['example_tag'].get('boolean'))
+ logging.info(iq.get('example_tag').get('boolean'))
+ #<<<<<<<<<<<<
+
+Z rozszerzenia ExampleTag, dostęp do elementów jest podobny, tyle że, nie wymagane jest określanie tagu, którego dotyczy. Dodatkową zaletą jest fakt niejednolitego dostępu, na przykład do parametru `'text'` między rozpoczęciem a zakończeniem tagu. Pokazuje to poniższy przykład, ujednolicając metody obiektowych getterów i setterów.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ Nazwa głównego pliku XML tego rozszerzenia.
+ namespace = "https://example.net/our_extension" ##~ Nazwa obiektu, np. <example_tag xmlns={namespace} (...)</example_tag>. Powinna zostać zmieniona na własną.
+
+ plugin_attrib = "example_tag" ##~ Nazwa, którą można odwołać się do obiektu. W szczególności, do zarejestrowanego obiektu można odwołać się przez: nazwa_obiektu['tag']. gdzie `'tag'` jest nazwą ElementBase extension. Nazwa powinna być taka sama jak "name" wyżej.
+
+ interfaces = {"boolean", "some_string"} ##~ Lista kluczy słownika, które mogą być użyte z obiektem. Na przykład: `stanza_object['example_tag']` zwraca {"another": "some", "data": "some"}, gdzie `'example_tag'` jest nazwą rozszerzenia ElementBase.
+
+ #>>>>>>>>>>>>
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+ #<<<<<<<<<<<<
+
+Atrybut `'self.xml'` jest dziedziczony z klasy `'ElementBase'` i jest to dosłownie `'Element'` z pakietu `'ElementTree'`.
+
+Kiedy odpowiednie gettery i settery są tworzone, można sprawdzić, czy na pewno podany argument spełnia normy pluginu lub konwersję na pożądany typ. Dodatkowo, kod staje się bardziej przejrzysty w standardach programowania obiektowego, jak na poniższym przykładzie:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag']['boolean'] = "True" #Przypisanie wprost
+ #>>>>>>>>>>>>
+ iq['example_tag'].set_some_string("Another_string") #Przypisanie poprzez setter
+ iq['example_tag'].set_text("Info_inside_tag")
+ #<<<<<<<<<<<<
+ iq.send()
+
+Wczytanie ExampleTag ElementBase z pliku XML, łańcucha znaków i innych obiektów
+--------------------------------------------------------------------------------
+
+Jest wiele możliwości na wczytanie wcześniej zdefiniowanego napisu z pliku albo lxml (ElementTree). Poniższy przykład wykorzystuje parsowanie typu tekstowego do lxml (ElementTree) i przekazanie atrybutów.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ #...
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+ #...
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ Nazwa głównego pliku XML tego rozszerzenia.
+ namespace = "https://example.net/our_extension" ##~ Nazwa obiektu, np. <example_tag xmlns={namespace} (...)</example_tag>. Powinna zostać zmieniona na własną.
+
+ plugin_attrib = "example_tag" ##~ Nazwa, którą można odwołać się do obiektu. W szczególności, do zarejestrowanego obiektu można odwołać się przez: nazwa_obiektu['tag']. gdzie `'tag'` jest nazwą ElementBase extension. Nazwa powinna być taka sama jak "name" wyżej.
+
+ interfaces = {"boolean", "some_string"} ##~ Lista kluczy słownika, które mogą być użyte z obiektem. Na przykład: `stanza_object['example_tag']` zwraca {"another": "some", "data": "some"}, gdzie `'example_tag'` jest nazwą rozszerzenia ElementBase.
+
+ #>>>>>>>>>>>>
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+ #<<<<<<<<<<<<
+
+Do przetestowania tej funkcjonalności, potrzebny jest pliku zawierający xml z tagiem, przykładowy napis z xml oraz przykładowy lxml (ET):
+
+.. code-block:: xml
+
+ #File: $WORKDIR/test_example_tag.xml
+
+ <example_tag xmlns="https://example.net/our_extension" some_string="StringFromFile">Info_inside_tag<inside_tag first_field="3" secound_field="4" /></example_tag>
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ #...
+ from slixmpp.xmlstream import ET
+ #...
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ self.disconnect_counter = 3 # Ta zmienna służy tylko do rozłączenia klienta po otrzymaniu odpowiedniej ilości odpowiedzi z Iq.
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Przykład rozłączania się aplikacji po uzyskaniu odpowiedniej ilości odpowiedzi.
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+ #<<<<<<<<<<<<
+
+Jeśli Responder zwróci wysłane Iq, a Sender wyłączy się po trzech odpowiedziach, wtedy wszystko działa tak, jak powinno.
+
+Łatwość użycia pluginu dla programistów
+----------------------------------------
+
+Każdy plugin powinien posiadać pewne obiektowe metody: wczytanie danych, jak w przypadku metod `setup` z poprzedniego rozdziału, gettery, settery, czy wywoływanie odpowiednich wydarzeń.
+Potencjalne błędy powinny być przechwytywane z poziomu pluginu i zwracane z odpowiednim opisem błędu w postaci odpowiedzi Iq o tym samym id do wysyłającego. Aby uniknąć sytuacji kiedy plugin nie robi tego co powinien, a wiadomość zwrotna nigdy nie nadchodzi, wysyłający dostaje error z komunikatem timeout.
+
+Poniżej przykład kodu podyktowanego tymi zasadami:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ Tekst czytelny dla człowieka oraz do znalezienia pluginu przez inny plugin.
+ self.xep = "ope" ##~ Tekst czytelny dla człowieka oraz do znalezienia pluginu przez inny plugin poprzez dodanie go do `slixmpp/plugins/__init__.py` do funkcji `__all__` z 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Nazwa tego Callbacku
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'get' oraz example_tag
+ self.__handle_get_iq)) ##~ Metoda przechwytuje odpowiednie Iq, powinna wywołać zdarzenie u klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Nazwa tego Callbacku
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'result' oraz example_tag
+ self.__handle_get_iq)) ##~ Metoda przechwytuje odpowiednie Iq, powinna wywołać zdarzenie u klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Nazwa tego Callbacku
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Obsługuje tylko Iq o typie 'error' oraz example_tag
+ self.__handle_get_iq)) ##~ Metoda przechwytuje odpowiednie Iq, powinna wywołać zdarzenie u klienta.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Nazwa tego Callbacku
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Obsługuje tylko Message z example_tag
+ self.__handle_message)) ##~ Metoda przechwytuje odpowiednie Iq, powinna wywołać zdarzenie u klienta.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Zarejestrowane rozszerzenia tagu dla Iq. Bez tego, iq['example_tag'] będzie polem tekstowym, a nie kontenerem i nie będzie można zmieniać w nim pól i tworzyć pod-elementów.
+ register_stanza_plugin(Message, ExampleTag) ##~ Zarejestrowane rozszerzenia tagu dla wiadomości Message. Bez tego, message['example_tag'] będzie polem tekstowym, a nie kontenerem i nie będzie można zmieniać w nim pól i tworzyć pod-elementów.
+
+ # Wszystkie możliwe typy iq: get, set, error, result
+ def __handle_get_iq(self, iq):
+ if iq.get_some_string is None:
+ error = iq.reply(clear=False)
+ error["type"] = "error"
+ error["error"]["condition"] = "missing-data"
+ error["error"]["text"] = "Without some_string value returns error."
+ error.send()
+ # Zrób coś z otrzymanym Iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Wywołanie zdarzenia, które może być przesłane do klienta lub zmienione po drodze.
+
+ def __handle_result_iq(self, iq):
+ # Zrób coś z otrzymanym Iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Wywołanie zdarzenia, które może być przesłany do klienta lub zmienione po drodze.
+
+ def __handle_error_iq(self, iq):
+ # Zrób coś z otrzymanym Iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Wywołanie zdarzenia, które może być przesłane do klienta lub zmienione po drodze.
+
+ def __handle_message(self, msg):
+ # Zrób coś z otrzymaną wiadomością
+ self.xmpp.event('example_tag_message', msg) ##~ Wywołanie zdarzenia, które może być przesłane do klienta lub zmienione po drodze.
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ Nazwa głównego pliku XML tego rozszerzenia.
+ namespace = "https://example.net/our_extension" ##~ Nazwa obiektu, np. <example_tag xmlns={namespace} (...)</example_tag>. Powinna zostać zmieniona na własną.
+
+ plugin_attrib = "example_tag" ##~ Nazwa, którą można odwołać się do obiektu. W szczególności, do zarejestrowanego obiektu można odwołać się przez: nazwa_obiektu['tag']. gdzie `'tag'` jest nazwą ElementBase extension. Nazwa powinna być taka sama jak "name" wyżej.
+
+ interfaces = {"boolean", "some_string"} ##~ Lista kluczy słownika, które mogą być użyte z obiektem. Na przykład: `stanza_object['example_tag']` zwraca {"another": "some", "data": "some"}, gdzie `'example_tag'` jest nazwą rozszerzenia ElementBase.
+
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+
+ def setup_from_dict(self, data):
+ #Poprawnośc kluczy słownika powinna być sprawdzona
+ self.xml.attrib.update(data)
+
+ def get_boolean(self):
+ return self.xml.attrib.get("boolean", None)
+
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+
+ def fill_interfaces(self, boolean, some_string):
+ #Jakaś walidacja, jeśli jest potrzebna
+ self.set_boolean(boolean)
+ self.set_some_string(some_string)
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+
+ import slixmpp
+ import example_plugin
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_get_iq", self.example_tag_get_iq)
+ self.add_event_handler("example_tag_message", self.example_tag_message)
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online
+ self.send_presence()
+ self.get_roster()
+
+ def example_tag_get_iq(self, iq): # Iq zawsze powinien odpowiedzieć. Jeżeli użytkownik jest offline, zostanie zwrócony error.
+ logging.info(iq)
+ reply = iq.reply()
+ reply["example_tag"].fill_interfaces(True, "Reply_string")
+ reply.send()
+
+ def example_tag_message(self, msg):
+ logging.info(msg) # Na wiadomość Message można odpowiedzieć, ale nie trzeba.
+
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Responder.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message to")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Responder(args.jid, args.password)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPluggin jest nazwa klasy example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Dwie niewymagane metody pozwalające innym użytkownikom zobaczyć dostępność online
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 5 # Aplikacja rozłączy się po odebraniu takiej ilości odpowiedzi.
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Przykład rozłączania się aplikacji po uzyskaniu odpowiedniej ilości odpowiedzi.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Przykład rozłączania się aplikacji po uzyskaniu odpowiedniej ilości odpowiedzi.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # Kiedy, aby otrzymać odpowiedż z błędem, potrzebny jest example_tag bez wartości bool.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin jest nazwą klasy z example_plugin.
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+
+Tagi i atrybuty zagnieżdżone wewnątrz głównego elementu
+---------------------------------------------------------
+
+Aby stworzyć zagnieżdżony tag, wewnątrz głównego tagu, rozważmy atrybut `'self.xml'` jako Element z ET (ElementTree). W takim wypadku, aby stworzyć zagnieżdżony element można użyć funkcji 'append'.
+
+Można powtórzyć poprzednie działania inicjalizując nowy element jak główny (ExampleTag). Jednak jeśli nie potrzebujemy dodatkowych metod, czy walidacji, a jest to wynik dla innego procesu który i tak będzie parsował xml, wtedy możemy zagnieździć zwyczajny Element z ElementTree za pomocą metody `'append'`. W przypadku przetwarzania typu tekstowego, można to zrobić nawet dzięki parsowaniu napisu na Element - kolejne zagnieżdżenia już będą w dodanym Elemencie do głównego. By nie powtarzać metody setup, poniżej przedstawione jest ręczne dodanie zagnieżdżonego taga konstruując ET.Element samodzielnie.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+
+ #(...)
+
+ class ExampleTag(ElementBase):
+
+ #(...)
+
+ def add_inside_tag(self, tag, attributes, text=""):
+ #Można rozszerzyć tag o tagi wewnętrzne do tagu, na przykład tak:
+ itemXML = ET.Element("{{{0:s}}}{1:s}".format(self.namespace, tag)) #~ Inicjalizujemy Element z wewnętrznym tagiem, na przykład: <example_tag (...)> <inside_tag namespace="https://example.net/our_extension"/></example_tag>
+ itemXML.attrib.update(attributes) #~ Przypisujemy zdefiniowane atrybuty, na przykład: <inside_tag namespace=(...) inner_data="some"/>
+ itemXML.text = text #~ Dodajemy text wewnątrz tego tagu: <inside_tag (...)>our_text</inside_tag>
+ self.xml.append(itemXML) #~ I tak skonstruowany Element po prostu dodajemy do elementu z tagiem `example_tag`.
+
+Można też zrobić to samo używając słownika i nazw jako kluczy zagnieżdżonych elementów. W takim przypadku, pola funkcji powinny zostać przeniesione do ET.
+
+Kompletny kod tutorialu
+-------------------------
+
+W poniższym kodzie zostały pozostawione oryginalne komentarze w języku angielskim.
+
+.. code-block:: python
+
+#!/usr/bin/python3
+#File: /usr/bin/test_slixmpp & permissions rwx--x--x (711)
+
+import subprocess
+import time
+
+if __name__ == "__main__":
+ #~ prefix = ["x-terminal-emulator", "-e"] # Separate terminal for every client; can be replaced with other terminal
+ #~ prefix = ["xterm", "-e"]
+ prefix = []
+ #~ suffix = ["-d"] # Debug
+ #~ suffix = ["-q"] # Quiet
+ suffix = []
+
+ sender_path = "./example/sender.py"
+ sender_jid = "SENDER_JID"
+ sender_password = "SENDER_PASSWORD"
+
+ example_file = "./test_example_tag.xml"
+
+ responder_path = "./example/responder.py"
+ responder_jid = "RESPONDER_JID"
+ responder_password = "RESPONDER_PASSWORD"
+
+ # Remember about the executable permission. (`chmod +x ./file.py`)
+ SENDER_TEST = prefix + [sender_path, "-j", sender_jid, "-p", sender_password, "-t", responder_jid, "--path", example_file] + suffix
+ RESPON_TEST = prefix + [responder_path, "-j", responder_jid, "-p", responder_password] + suffix
+
+ try:
+ responder = subprocess.Popen(RESPON_TEST)
+ sender = subprocess.Popen(SENDER_TEST)
+ responder.wait()
+ sender.wait()
+ except:
+ try:
+ responder.terminate()
+ except NameError:
+ pass
+ try:
+ sender.terminate()
+ except NameError:
+ pass
+ raise
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data for Human readable and find plugin by another plugin with method.
+ self.xep = "ope" ##~ String data for Human readable and find plugin by another plugin with adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'. Otherwise it's just human readable annotation.
+
+ namespace = ExampleTag.namespace
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Handle only Iq with type get and example_tag
+ self.__handle_get_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleResult Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=result/{{{namespace}}}example_tag"), ##~ Handle only Iq with type result and example_tag
+ self.__handle_result_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleError Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=error/{{{namespace}}}example_tag"), ##~ Handle only Iq with type error and example_tag
+ self.__handle_error_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Name of this Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Handle only Message with example_tag
+ self.__handle_message)) ##~ Method which catch proper Message, should raise proper event for client.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Register tags extension for Iq object, otherwise iq['example_tag'] will be string field instead container where we can manage our fields and create sub elements.
+ register_stanza_plugin(Message, ExampleTag) ##~ Register tags extension for Message object, otherwise message['example_tag'] will be string field instead container where we can manage our fields and create sub elements.
+
+ # All iq types are: get, set, error, result
+ def __handle_get_iq(self, iq):
+ if iq.get_some_string is None:
+ error = iq.reply(clear=False)
+ error["type"] = "error"
+ error["error"]["condition"] = "missing-data"
+ error["error"]["text"] = "Without some_string value returns error."
+ error.send()
+ # Do something with received iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_result_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_error_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_message(self, msg):
+ # Do something with received message
+ self.xmpp.event('example_tag_message', msg) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The namespace our stanza object lives in, like <example_tag xmlns={namespace} (...)</example_tag>. You should change it for your own namespace
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'] now `'example_tag'` is name of ours ElementBase extension. And this should be that same as name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ours ElementBase extension.
+
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+
+ def setup_from_dict(self, data):
+ #There should keys should be also validated
+ self.xml.attrib.update(data)
+
+ def get_boolean(self):
+ return self.xml.attrib.get("boolean", None)
+
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+
+ def fill_interfaces(self, boolean, some_string):
+ #Some validation if it is necessary
+ self.set_boolean(boolean)
+ self.set_some_string(some_string)
+
+ def add_inside_tag(self, tag, attributes, text=""):
+ #If we want to fill with additionaly tags our element, then we can do it that way for example:
+ itemXML = ET.Element("{{{0:s}}}{1:s}".format(self.namespace, tag)) #~ Initialize ET with our tag, for example: <example_tag (...)> <inside_tag namespace="https://example.net/our_extension"/></example_tag>
+ itemXML.attrib.update(attributes) #~ There we add some fields inside tag, for example: <inside_tag namespace=(...) inner_data="some"/>
+ itemXML.text = text #~ Fill field inside tag, for example: <inside_tag (...)>our_text</inside_tag>
+ self.xml.append(itemXML) #~ Add that all what we set, as inner tag inside `example_tag` tag.
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 6 # This is only for disconnect when we receive all replies for sended Iq
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_iq_with_inner_tag(self.to)
+ # <iq from="SENDER/RESOURCE" to="RESPONDER/RESOURCE" id="1" xml:lang="en" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_iq_with_inner_tag(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=1)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+
+ inner_attributes = {"first_field": "1", "secound_field": "2"}
+ iq['example_tag'].add_inside_tag(tag="inside_tag", attributes=inner_attributes)
+
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # For example, our condition to receive error respond is example_tag without boolean value.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 6 # This is only for disconnect when we receive all replies for sended Iq
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_iq_with_inner_tag(self.to)
+ # <iq from="SENDER/RESOURCE" to="RESPONDER/RESOURCE" id="1" xml:lang="en" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" secound_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag secound_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_iq_with_inner_tag(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=1)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+
+ inner_attributes = {"first_field": "1", "secound_field": "2"}
+ iq['example_tag'].add_inside_tag(tag="inside_tag", attributes=inner_attributes)
+
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # For example, our condition to receive error respond is example_tag without boolean value.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/test_example_tag.xml
+
+.. code-block:: xml
+
+ <example_tag xmlns="https://example.net/our_extension" some_string="StringFromFile">Info_inside_tag<inside_tag first_field="3" secound_field="4" /></example_tag>
+
+Źródła i bibliogarfia
+----------------------
+
+Slixmpp - opis projektu:
+
+* https://pypi.org/project/slixmpp/
+
+Oficjalna strona z dokumentacją:
+
+* https://slixmpp.readthedocs.io/
+
+Oficjalna dokumentacja PDF:
+
+* https://buildmedia.readthedocs.org/media/pdf/slixmpp/latest/slixmpp.pdf
+
+Dokumentacje w formie Web i PDF różnią się; pewne szczegóły potrafią być wspomniane tylko w jednej z dwóch.
diff --git a/docs/howto/make_plugin_extension_for_message_and_iq.rst b/docs/howto/make_plugin_extension_for_message_and_iq.rst
new file mode 100644
index 00000000..3a7784b8
--- /dev/null
+++ b/docs/howto/make_plugin_extension_for_message_and_iq.rst
@@ -0,0 +1,1796 @@
+How to make a slixmpp plugins for Messages and IQ extensions
+====================================================================
+
+Introduction and requirements
+------------------------------
+
+* `'python3'`
+
+Code used in the following tutorial is written in python 3.6 or newer.
+For backward compatibility, replace the f-strings functionality with older string formatting: `'"{}".format("content")'` or `'%s, "content"'`.
+
+Ubuntu linux installation steps:
+
+.. code-block:: bash
+
+ sudo apt-get install python3.6
+
+* `'slixmpp'`
+* `'argparse'`
+* `'logging'`
+* `'subprocess'`
+
+Check if these libraries and the proper python version are available at your environment. Every one of these, except the slixmpp, is a standard python library. However, it may happen that some of them may not be installed.
+
+.. code-block:: python
+
+ python3 --version
+ python3 -c "import slixmpp; print(slixmpp.__version__)"
+ python3 -c "import argparse; print(argparse.__version__)"
+ python3 -c "import logging; print(logging.__version__)"
+ python3 -m subprocess
+
+Example output:
+
+.. code-block:: bash
+
+ ~ $ python3 --version
+ Python 3.8.0
+ ~ $ python3 -c "import slixmpp; print(slixmpp.__version__)"
+ 1.4.2
+ ~ $ python3 -c "import argparse; print(argparse.__version__)"
+ 1.1
+ ~ $ python3 -c "import logging; print(logging.__version__)"
+ 0.5.1.2
+ ~ $ python3 -m subprocess #Should return nothing
+
+If some of the libraries throw `'ImportError'` or `'no module named ...'` error, install them with:
+
+On Ubuntu linux:
+
+.. code-block:: bash
+
+ pip3 install slixmpp
+ #or
+ easy_install slixmpp
+
+If some of the libraries throws NameError, reinstall the whole package once again.
+
+* `Jabber accounts`
+
+For the testing purposes, two private jabber accounts are required. They can be created on one of many available sites:
+https://www.google.com/search?q=jabber+server+list
+
+Client launch script
+-----------------------------
+
+The client launch script should be created outside of the main project location. This allows to easily obtain the results when needed and prevents accidental leakage of credential details to the git platform.
+
+As the example, a file `'test_slixmpp'` can be created in `'/usr/bin'` directory, with executive permission:
+
+.. code-block:: bash
+
+ /usr/bin $ chmod 711 test_slixmpp
+
+This file contains a simple structure for logging credentials:
+
+.. code-block:: python
+
+#!/usr/bin/python3
+#File: /usr/bin/test_slixmpp & permissions rwx--x--x (711)
+
+import subprocess
+import time
+
+if __name__ == "__main__":
+ #~ prefix = ["x-terminal-emulator", "-e"] # Separate terminal for every client; can be replaced with other terminal
+ #~ prefix = ["xterm", "-e"]
+ prefix = []
+ #~ suffix = ["-d"] # Debug
+ #~ suffix = ["-q"] # Quiet
+ suffix = []
+
+ sender_path = "./example/sender.py"
+ sender_jid = "SENDER_JID"
+ sender_password = "SENDER_PASSWORD"
+
+ example_file = "./test_example_tag.xml"
+
+ responder_path = "./example/responder.py"
+ responder_jid = "RESPONDER_JID"
+ responder_password = "RESPONDER_PASSWORD"
+
+ # Remember about the executable permission. (`chmod +x ./file.py`)
+ SENDER_TEST = prefix + [sender_path, "-j", sender_jid, "-p", sender_password, "-t", responder_jid, "--path", example_file] + suffix
+ RESPON_TEST = prefix + [responder_path, "-j", responder_jid, "-p", responder_password] + suffix
+
+ try:
+ responder = subprocess.Popen(RESPON_TEST)
+ sender = subprocess.Popen(SENDER_TEST)
+ responder.wait()
+ sender.wait()
+ except:
+ try:
+ responder.terminate()
+ except NameError:
+ pass
+ try:
+ sender.terminate()
+ except NameError:
+ pass
+ raise
+
+The launch script should be convenient in use and easy to reconfigure again. The proper preparation of it now, can help saving time in the future. Logging credentials, the project paths (from `'sys.argv[...]'` or `'os.getcwd()'`), set the parameters for the debugging purposes, mock the testing xml file and many more things can be defined inside. Whichever parameters are used, the script testing itself should be fast and effortless. The proper preparation of it now, can help saving time in the future.
+
+In case of manually testing the larger applications, it would be a good practice to introduce the unique names (consequently, different commands) for each client. In case of any errors, it will be easier to find the client that caused it.
+
+Creating the client and the plugin
+-----------------------------------
+
+Two slixmpp clients should be created in order to check if everything works correctly (here: the `'sender'` and the `'responder'`). The minimal amount of code needed for effective building and testing of the plugin is the following:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see if the client is online.
+ self.send_presence()
+ self.get_roster()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ #xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is the example_plugin class name.
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+
+ import slixmpp
+ import example_plugin
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see if the client is online.
+ self.send_presence()
+ self.get_roster()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Responder.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message to")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Responder(args.jid, args.password)
+ #xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is the example_plugin class name.
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+Next file to create is `'example_plugin.py'`. It can be placed in the same folder as the clients, so the problems with unknown paths can be avoided.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data readable by humans and to find plugin by another plugin.
+ self.xep = "ope" ##~ String data readable by humans and to find plugin by another plugin by adding it into `slixmpp/plugins/__init__.py` to the `__all__` field, with 'xep_OPE' prefix.
+
+ namespace = ExampleTag.namespace
+
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element for that extension.
+ namespace = "<https://example.net/our_extension>" ##~ The namespace of the object, like <example_tag xmlns={namespace} (...)</example_tag>. Should be changed to your namespace.
+
+ plugin_attrib = "example_tag" ##~ The name under which the data in plugin can be accessed. In particular, this object is reachable from the outside with: stanza_object['example_tag']. The `'example_tag'` is name of ElementBase extension and should be that same as the name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ElementBase extension.
+
+If the plugin is not in the same directory as the clients, then the symbolic link to the localisation reachable by the clients should be established:
+
+.. code-block:: bash
+
+ ln -s $Path_to_example_plugin_py $Path_to_clients_destinations
+
+The other solution is to relative import it (with dots '.') to get the proper path.
+
+First run and the event handlers
+-------------------------------------------------
+
+To check if everything is okay, the `'start'` method can be used(which triggers the `'session_start'` event). Right after the client is ready, the signal will be sent.
+
+In the `'__init__'` method, the handler for event call `'session_start'` is created. When it is called, the `'def start(self, event):'` method will be executed. During the first run, add the line: `'logging.info("I'm running")'` to both the sender and the responder, and use `'test_slixmpp'` command.
+
+The `'def start(self, event):'` method should look like this:
+
+.. code-block:: python
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ logging.info("I'm running")
+ #<<<<<<<<<<<<
+
+If everything works fine, this line can be commented out.
+
+Building the message object
+------------------------------
+
+The example sender class should get a recipient name and address (jid of responder) from command line arguments, stored in test_slixmpp. An access to this argument is stored in the `'self.to'` attribute.
+
+Code example:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+ #>>>>>>>>>>>>
+ self.send_example_message(self.to, "example_message")
+
+ def send_example_message(self, to, body):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Default mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ msg.send()
+ #<<<<<<<<<<<<
+
+In the example below, the build-in method `'make_message'` is used. It creates a string "example_message" and sends it at the end of `'start'` method. The message will be sent once, after the script launch.
+
+To receive this message, the responder should have a proper handler to the signal with the message object and the method to decide what to do with this message. As it is shown in the example below:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+
+ #>>>>>>>>>>>>
+ self.add_event_handler("message", self.message)
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ def message(self, msg):
+ #Show all inside msg
+ logging.info(msg)
+ #Show only body attribute
+ logging.info(msg['body'])
+ #<<<<<<<<<<<<
+
+Expanding the Message with a new tag
+-------------------------------------
+
+To expand the Message object with a tag, the plugin should be registered as the extension for the Message object:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data readable by humans and to find plugin by another plugin.
+ self.xep = "ope" ##~ String data readable by humans and to find plugin by another plugin by adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ #>>>>>>>>>>>>
+ register_stanza_plugin(Message, ExampleTag) ##~ Register the tag extension for Message object, otherwise message['example_tag'] will be string field instead container and managing fields and create sub elements would be impossible.
+ #<<<<<<<<<<<<
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The namespace for stanza object, like <example_tag xmlns={namespace} (...)</example_tag>.
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'] now `'example_tag'` is name of ElementBase extension. And this should be that same as 'name' above.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ours ElementBase extension.
+
+ #>>>>>>>>>>>>
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+ #<<<<<<<<<<<<
+
+Now, with the registered object, the message can be extended.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+ self.send_example_message(self.to, "example_message")
+
+ def send_example_message(self, to, body):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Default mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ #>>>>>>>>>>>>
+ msg['example_tag']['some_string'] = "Work!"
+ logging.info(msg)
+ #<<<<<<<<<<<<
+ msg.send()
+
+After running, the logging should print the Message with tag `'example_tag'` stored inside <message><example_tag/></message>, string `'Work'` and given namespace.
+
+Giving the extended message the separate signal
+------------------------------------------------
+
+If the separate event is not defined, then both normal and extended message will be cached by signal `'message'`. In order to have the special event, the handler for the namespace and tag should be created. Then, make a unique name combination, which allows the handler can catch only the wanted messages (or Iq object).
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data readable by humans and to find the plugin by another plugin.
+ self.xep = "ope" ##~ String data readable by humans and to find the plugin by another plugin by adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Name of this Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Handles only the Message with good example_tag and namespace.
+ self.__handle_message)) ##~ Method catches the proper Message, should raise event for the client.
+ register_stanza_plugin(Message, ExampleTag) ##~ Register the tags extension for Message object, otherwise message['example_tag'] will be string field instead container and managing the fields and create sub elements would not be possible.
+
+ def __handle_message(self, msg):
+ # Here something can be done with received message before it reaches the client.
+ self.xmpp.event('example_tag_message', msg) ##~ Call event which can be handled by the client with desired object as an argument.
+
+StanzaPath objects should be initialised in a specific way, such as:
+`'OBJECT_NAME[@type=TYPE_OF_OBJECT][/{NAMESPACE}[TAG]]'`
+
+* OBJECT_NAME can be `'message'` or `'iq'`.
+* For TYPE_OF_OBJECT, when iq is specified, `'get, set, error or result'` can be used. When object is a message, then the message type can be used, like `'chat'`.
+* NAMESPACE should always be a namespace from tag extension class.
+* TAG should contain the tag, in this case:`'example_tag'`.
+
+Now every message containing the defined namespace inside `'example_tag'` is cached. It is possible to check the content of it. Then, the message is send to the client with the `'example_tag_message'` event.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+ #>>>>>>>>>>>>
+ self.send_example_message(self.to, "example_message", "example_string")
+
+ def send_example_message(self, to, body, some_string=""):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ # Default mtype == "chat";
+ msg = self.make_message(mto=to, mbody=body)
+ if some_string:
+ msg['example_tag'].set_some_string(some_string)
+ msg.send()
+ #<<<<<<<<<<<<
+
+Now, remember the line: `'self.xmpp.event('example_tag_message', msg)'`. The name of an event to catch inside the "responder.py" file was defined here. Here it is: `'example_tag_message'`.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_message", self.example_tag_message) #Registration of the handler
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ def example_tag_message(self, msg):
+ logging.info(msg) # Message is standalone object, it can be replied, but no error is returned if not.
+ #<<<<<<<<<<<<
+
+The messages can be replied, but nothing will happen otherwise.
+The Iq object, on the other hand, should always be replied. Otherwise, the error occurs on the client side due to the target timeout if the cell Iq won't reply with Iq with the same Id.
+
+Useful methods and misc.
+-------------------------
+
+Modifying the example `Message` object to the `Iq` object
+----------------------------------------------------------
+
+To allow our custom element into Iq payloads, a new handler for Iq can be registered, in the same manner as in the `,,Extend message with tags''` part. The following example contains several types of Iq different types to catch. It can be used to check the difference between the Iq request and Iq response or to verify the correctness of the objects. All of the Iq messages should be passed to the sender with the same ID parameter, otherwise the sender will receive an error message.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data readable by humans and to find the plugin by another plugin.
+ self.xep = "ope" ##~ String data readable by humans and to find the plugin by another plugin by adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ #>>>>>>>>>>>>
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'get' and example_tag
+ self.__handle_get_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleResult Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=result/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'result' and example_tag
+ self.__handle_result_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleError Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=error/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'error' and example_tag
+ self.__handle_error_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Name of this Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Handle only Message with example_tag
+ self.__handle_message)) ##~ Method which catch proper Message, should raise proper event for client.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Register tags extension for Iq object. Otherwise the iq['example_tag'] will be string field instead of container and it would not be possible to manage the fields and sub elements.
+ #<<<<<<<<<<<<
+ register_stanza_plugin(Message, ExampleTag) ##~ Register tags extension for Message object, otherwise message['example_tag'] will be string field instead container, where it is impossible to manage fields and create sub elements.
+
+ #>>>>>>>>>>>>
+ # All iq types are: get, set, error, result
+ def __handle_get_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Calls the event which can be handled by clients.
+
+ def __handle_result_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Calls the event which can be handled by clients.
+
+ def __handle_error_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Calls the event which can be handled by clients.
+
+ def __handle_message(self, msg):
+ # Do something with received message
+ self.xmpp.event('example_tag_message', msg) ##~ Calls the event which can be handled by clients.
+
+The events called by the example handlers can be caught like in the`'example_tag_message'` part.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_message", self.example_tag_message)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_get_iq", self.example_tag_get_iq)
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def example_tag_get_iq(self, iq): # Iq stanza always should have a respond. If user is offline, it calls an error.
+ logging.info(str(iq))
+ reply = iq.reply(clear=False)
+ reply.send()
+ #<<<<<<<<<<<<
+
+By default, the parameter `'clear'` in the `'Iq.reply'` is set to True. In that case, the content of the Iq should be set again. After using the reply method, only the Id and the Jid parameters will still be set.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ #>>>>>>>>>>>>
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+ #<<<<<<<<<<<<
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag']['boolean'] = "True"
+ iq['example_tag']['some_string'] = "Another_string"
+ iq['example_tag'].text = "Info_inside_tag"
+ iq.send()
+ #<<<<<<<<<<<<
+
+ #>>>>>>>>>>>>
+ def example_tag_result_iq(self, iq):
+ logging.info(str(iq))
+
+ def example_tag_error_iq(self, iq):
+ logging.info(str(iq))
+ #<<<<<<<<<<<<
+
+Different ways to access the elements
+--------------------------------------
+
+There are several ways to access the elements inside the Message or Iq stanza. The first one: the client can access them like a dictionary:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ #...
+ def example_tag_result_iq(self, iq):
+ logging.info(str(iq))
+ #>>>>>>>>>>>>
+ logging.info(iq['id'])
+ logging.info(iq.get('id'))
+ logging.info(iq['example_tag']['boolean'])
+ logging.info(iq['example_tag'].get('boolean'))
+ logging.info(iq.get('example_tag').get('boolean'))
+ #<<<<<<<<<<<<
+
+The access to the elements from extended ExampleTag is similar. However, defining the types is not required and the access can be diversified (like for the `'text'` field below). For the ExampleTag extension, there is a 'getter' and 'setter' method for specific fields:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The namespace for stanza object, like <example_tag xmlns={namespace} (...)</example_tag>. Should be changed to own namespace.
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'], the `'example_tag'` is the name of ElementBase extension. And this should be the same as name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ElementBase extension.
+
+ #>>>>>>>>>>>>
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+ #<<<<<<<<<<<<
+
+The attribute `'self.xml'` is inherited from the ElementBase and is exactly the same as the `'Iq['example_tag']'` from the client namespace.
+
+When the proper setters and getters are used, it is easy to check whether some argument is proper for the plugin or is convertible to another type. The code itself can be cleaner and more object-oriented, like in the example below:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag']['boolean'] = "True" #Direct assignment
+ #>>>>>>>>>>>>
+ iq['example_tag'].set_some_string("Another_string") #Assignment by setter
+ iq['example_tag'].set_text("Info_inside_tag")
+ #<<<<<<<<<<<<
+ iq.send()
+
+Message setup from the XML files, strings and other objects
+------------------------------------------------------------
+
+There are many ways to set up a xml from a string, xml-containing file or lxml (ElementTree) file. One of them is parsing the strings to lxml object, passing the attributes and other information, which may look like this:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ #...
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+ #...
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The stanza object namespace, like <example_tag xmlns={namespace} (...)</example_tag>. Should be changed to your own namespace
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'] now `'example_tag'` is name of ElementBase extension. And this should be that same as name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ElementBase extension.
+
+ #>>>>>>>>>>>>
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+ #<<<<<<<<<<<<
+
+To test this, an example file with xml, example xml string and example lxml (ET) object is needed:
+
+.. code-block:: xml
+
+ #File: $WORKDIR/test_example_tag.xml
+
+ <example_tag xmlns="https://example.net/our_extension" some_string="StringFromFile">Info_inside_tag<inside_tag first_field="3" second_field="4" /></example_tag>
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ #...
+ from slixmpp.xmlstream import ET
+ #...
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ #>>>>>>>>>>>>
+ self.disconnect_counter = 3 # Disconnects when all replies from Iq are received.
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after receiving the maximum number of responses.
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+ #<<<<<<<<<<<<
+
+If the Responder returns the proper `'Iq'` and the Sender disconnects after three answers, then everything works okay.
+
+Dev friendly methods for plugin usage
+--------------------------------------
+
+Any plugin should have some sort of object-like methods, that was setup for elements: reading the data, getters, setters and signals, to make them easy to use.
+During handling, the correctness of the data should be checked and the eventual errors returned back to the sender. In order to avoid the situation where the answer message is never send, the sender gets the timeout error.
+
+The following code presents exactly this:
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example plugin.py
+
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data to read by humans and to find the plugin by another plugin.
+ self.xep = "ope" ##~ String data to read by humans and to find the plugin by another plugin by adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'.
+
+ namespace = ExampleTag.namespace
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'get' and example_tag
+ self.__handle_get_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleResult Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=result/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'result' and example_tag
+ self.__handle_result_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleError Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=error/{{{namespace}}}example_tag"), ##~ Handle only Iq with type 'error' and example_tag
+ self.__handle_error_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Name of this Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Handle only Message with example_tag
+ self.__handle_message)) ##~ Method which catch proper Message, should raise proper event for client.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Register tags extension for Iq object. Otherwise the iq['example_tag'] will be string field instead of container and it would not be possible to manage the fields and sub elements.
+ register_stanza_plugin(Message, ExampleTag) ##~ Register tags extension for Iq object. Otherwise the iq['example_tag'] will be string field instead of container and it would not be possible to manage the fields and sub elements.
+
+ # All iq types are: get, set, error, result
+ def __handle_get_iq(self, iq):
+ if iq.get_some_string is None:
+ error = iq.reply(clear=False)
+ error["type"] = "error"
+ error["error"]["condition"] = "missing-data"
+ error["error"]["text"] = "Without some_string value returns error."
+ error.send()
+ # Do something with received iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Call event which can be handled by clients to send or something else.
+
+ def __handle_result_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Call event which can be handled by clients to send or something else.
+
+ def __handle_error_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Call event which can be handled by clients to send or something else.
+
+ def __handle_message(self, msg):
+ # Do something with received message
+ self.xmpp.event('example_tag_message', msg) ##~ Call event which can be handled by clients to send or something else.
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The namespace stanza object lives in, like <example_tag xmlns={namespace} (...)</example_tag>. You should change it for your own namespace.
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'] now `'example_tag'` is name of ElementBase extension. And this should be that same as name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ElementBase extension.
+
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+
+ def setup_from_dict(self, data):
+ #There keys from dict should be also validated
+ self.xml.attrib.update(data)
+
+ def get_boolean(self):
+ return self.xml.attrib.get("boolean", None)
+
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+
+ def fill_interfaces(self, boolean, some_string):
+ #Some validation, if necessary
+ self.set_boolean(boolean)
+ self.set_some_string(some_string)
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+
+ import slixmpp
+ import example_plugin
+
+ class Responder(slixmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_get_iq", self.example_tag_get_iq)
+ self.add_event_handler("example_tag_message", self.example_tag_message)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ def example_tag_get_iq(self, iq): # Iq stanza always should have a respond. If user is offline, it call an error.
+ logging.info(iq)
+ reply = iq.reply()
+ reply["example_tag"].fill_interfaces(True, "Reply_string")
+ reply.send()
+
+ def example_tag_message(self, msg):
+ logging.info(msg) # Message is standalone object, it can be replied, but no error arrives if not.
+
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Responder.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message to")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Responder(args.jid, args.password)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 5 # # Disconnect after receiving the maximum number of responses.
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after receiving the maximum number of responses.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after receiving the maximum number of responses.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # For example, the condition to receive the error respond is the example_tag without the boolean value.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin.
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+Tags and strings nested inside the tag
+---------------------------------------------------------
+
+To create the nested element inside IQ tag, `self.xml` field can be considered as an Element from ET (ElementTree). Therefore adding the nested Elements is appending the Element.
+
+As shown in the previous examples, it is possible to create a new element as main (ExampleTag). However, when the additional methods or validation is not needed and the result will be parsed to xml anyway, it may be better to nest the Element from ElementTree with method 'append'. In order to not use the 'setup' method again, the code below shows way of the manual addition of the nested tag and creation of ET Element.
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+
+ #(...)
+
+ class ExampleTag(ElementBase):
+
+ #(...)
+
+ def add_inside_tag(self, tag, attributes, text=""):
+ #If more tags is needed inside the element, they can be added like that:
+ itemXML = ET.Element("{{{0:s}}}{1:s}".format(self.namespace, tag)) #~ Initialise ET with tag, for example: <example_tag (...)> <inside_tag namespace="<https://example.net/our_extension>"/></example_tag>
+ itemXML.attrib.update(attributes) #~ Here we add some fields inside tag, for example: <inside_tag namespace=(...) inner_data="some"/>
+ itemXML.text = text #~ Fill field inside tag, for example: <inside_tag (...)>our_text</inside_tag>
+ self.xml.append(itemXML) #~ Add that is all, what needs to be set as an inner tag inside the `example_tag` tag.
+
+There is a way to do this with a dictionary and name for the nested element tag. In that case, the insides of the function fields should be transferred to the ET element.
+
+Complete code from tutorial
+----------------------------
+
+.. code-block:: python
+
+#!/usr/bin/python3
+#File: /usr/bin/test_slixmpp & permissions rwx--x--x (711)
+
+import subprocess
+import time
+
+if __name__ == "__main__":
+ #~ prefix = ["x-terminal-emulator", "-e"] # Separate terminal for every client; can be replaced with other terminal
+ #~ prefix = ["xterm", "-e"]
+ prefix = []
+ #~ suffix = ["-d"] # Debug
+ #~ suffix = ["-q"] # Quiet
+ suffix = []
+
+ sender_path = "./example/sender.py"
+ sender_jid = "SENDER_JID"
+ sender_password = "SENDER_PASSWORD"
+
+ example_file = "./test_example_tag.xml"
+
+ responder_path = "./example/responder.py"
+ responder_jid = "RESPONDER_JID"
+ responder_password = "RESPONDER_PASSWORD"
+
+ # Remember about the executable permission. (`chmod +x ./file.py`)
+ SENDER_TEST = prefix + [sender_path, "-j", sender_jid, "-p", sender_password, "-t", responder_jid, "--path", example_file] + suffix
+ RESPON_TEST = prefix + [responder_path, "-j", responder_jid, "-p", responder_password] + suffix
+
+ try:
+ responder = subprocess.Popen(RESPON_TEST)
+ sender = subprocess.Popen(SENDER_TEST)
+ responder.wait()
+ sender.wait()
+ except:
+ try:
+ responder.terminate()
+ except NameError:
+ pass
+ try:
+ sender.terminate()
+ except NameError:
+ pass
+ raise
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/example_plugin.py
+
+ import logging
+
+ from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+ from slixmpp import Iq
+ from slixmpp import Message
+
+ from slixmpp.plugins.base import BasePlugin
+
+ from slixmpp.xmlstream.handler import Callback
+ from slixmpp.xmlstream.matcher import StanzaPath
+
+ log = logging.getLogger(__name__)
+
+ class OurPlugin(BasePlugin):
+ def plugin_init(self):
+ self.description = "OurPluginExtension" ##~ String data for Human readable and find plugin by another plugin with method.
+ self.xep = "ope" ##~ String data for Human readable and find plugin by another plugin with adding it into `slixmpp/plugins/__init__.py` to the `__all__` declaration with 'xep_OPE'. Otherwise it's just human readable annotation.
+
+ namespace = ExampleTag.namespace
+ self.xmpp.register_handler(
+ Callback('ExampleGet Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=get/{{{namespace}}}example_tag"), ##~ Handle only Iq with type get and example_tag
+ self.__handle_get_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleResult Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=result/{{{namespace}}}example_tag"), ##~ Handle only Iq with type result and example_tag
+ self.__handle_result_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleError Event:example_tag', ##~ Name of this Callback
+ StanzaPath(f"iq@type=error/{{{namespace}}}example_tag"), ##~ Handle only Iq with type error and example_tag
+ self.__handle_error_iq)) ##~ Method which catch proper Iq, should raise proper event for client.
+
+ self.xmpp.register_handler(
+ Callback('ExampleMessage Event:example_tag',##~ Name of this Callback
+ StanzaPath(f'message/{{{namespace}}}example_tag'), ##~ Handle only Message with example_tag
+ self.__handle_message)) ##~ Method which catch proper Message, should raise proper event for client.
+
+ register_stanza_plugin(Iq, ExampleTag) ##~ Register tags extension for Iq object. Otherwise the iq['example_tag'] will be string field instead of container and it would not be possible to manage the fields and sub elements.
+ register_stanza_plugin(Message, ExampleTag) ##~ Register tags extension for Iq object. Otherwise the iq['example_tag'] will be string field instead of container and it would not be possible to manage the fields and sub elements.
+
+ # All iq types are: get, set, error, result
+ def __handle_get_iq(self, iq):
+ if iq.get_some_string is None:
+ error = iq.reply(clear=False)
+ error["type"] = "error"
+ error["error"]["condition"] = "missing-data"
+ error["error"]["text"] = "Without some_string value returns error."
+ error.send()
+ # Do something with received iq
+ self.xmpp.event('example_tag_get_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_result_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_result_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_error_iq(self, iq):
+ # Do something with received iq
+ self.xmpp.event('example_tag_error_iq', iq) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ def __handle_message(self, msg):
+ # Do something with received message
+ self.xmpp.event('example_tag_message', msg) ##~ Call event which can be handled by clients to send or something other what you want.
+
+ class ExampleTag(ElementBase):
+ name = "example_tag" ##~ The name of the root XML element of that extension.
+ namespace = "https://example.net/our_extension" ##~ The stanza object namespace, like <example_tag xmlns={namespace} (...)</example_tag>. Should be changed for your namespace.
+
+ plugin_attrib = "example_tag" ##~ The name to access this type of stanza. In particular, given a registration stanza, the Registration object can be found using: stanza_object['example_tag'] now `'example_tag'` is name of ours ElementBase extension. And this should be that same as name.
+
+ interfaces = {"boolean", "some_string"} ##~ A list of dictionary-like keys that can be used with the stanza object. For example `stanza_object['example_tag']` gives us {"another": "some", "data": "some"}, whenever `'example_tag'` is name of ours ElementBase extension.
+
+ def setup_from_string(self, string):
+ """Initialize tag element from string"""
+ et_extension_tag_xml = ET.fromstring(string)
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_file(self, path):
+ """Initialize tag element from file containing adjusted data"""
+ et_extension_tag_xml = ET.parse(path).getroot()
+ self.setup_from_lxml(et_extension_tag_xml)
+
+ def setup_from_lxml(self, lxml):
+ """Add ET data to self xml structure."""
+ self.xml.attrib.update(lxml.attrib)
+ self.xml.text = lxml.text
+ self.xml.tail = lxml.tail
+ for inner_tag in lxml:
+ self.xml.append(inner_tag)
+
+ def setup_from_dict(self, data):
+ #There should keys should be also validated
+ self.xml.attrib.update(data)
+
+ def get_boolean(self):
+ return self.xml.attrib.get("boolean", None)
+
+ def get_some_string(self):
+ return self.xml.attrib.get("some_string", None)
+
+ def get_text(self, text):
+ return self.xml.text
+
+ def set_boolean(self, boolean):
+ self.xml.attrib['boolean'] = str(boolean)
+
+ def set_some_string(self, some_string):
+ self.xml.attrib['some_string'] = some_string
+
+ def set_text(self, text):
+ self.xml.text = text
+
+ def fill_interfaces(self, boolean, some_string):
+ #Some validation if it is necessary
+ self.set_boolean(boolean)
+ self.set_some_string(some_string)
+
+ def add_inside_tag(self, tag, attributes, text=""):
+ #If more tags is needed inside the element, they can be added like that:
+ itemXML = ET.Element("{{{0:s}}}{1:s}".format(self.namespace, tag)) #~ Initialise ET with tag, for example: <example_tag (...)> <inside_tag namespace="https://example.net/our_extension"/></example_tag>
+ itemXML.attrib.update(attributes) #~ There we add some fields inside tag, for example: <inside_tag namespace=(...) inner_data="some"/>
+ itemXML.text = text #~ Fill field inside tag, for example: <inside_tag (...)>our_text</inside_tag>
+ self.xml.append(itemXML) #~ Add that all what we set, as inner tag inside `example_tag` tag.
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/sender.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 6 # This is only for disconnect when we receive all replies for sent Iq
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_iq_with_inner_tag(self.to)
+ # <iq from="SENDER/RESOURCE" to="RESPONDER/RESOURCE" id="1" xml:lang="en" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_iq_with_inner_tag(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=1)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+
+ inner_attributes = {"first_field": "1", "second_field": "2"}
+ iq['example_tag'].add_inside_tag(tag="inside_tag", attributes=inner_attributes)
+
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # For example, the condition to receive error respond is the example_tag without boolean value.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/example/responder.py
+
+ import logging
+ from argparse import ArgumentParser
+ from getpass import getpass
+ import time
+
+ import slixmpp
+ from slixmpp.xmlstream import ET
+
+ import example_plugin
+
+ class Sender(slixmpp.ClientXMPP):
+ def __init__(self, jid, password, to, path):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ self.to = to
+ self.path = path
+
+ self.add_event_handler("session_start", self.start)
+ self.add_event_handler("example_tag_result_iq", self.example_tag_result_iq)
+ self.add_event_handler("example_tag_error_iq", self.example_tag_error_iq)
+
+ def start(self, event):
+ # Two, not required methods, but allows another users to see us available, and receive that information.
+ self.send_presence()
+ self.get_roster()
+
+ self.disconnect_counter = 6 # This is only for disconnect when we receive all replies for sended Iq
+
+ self.send_example_iq(self.to)
+ # <iq to=RESPONDER/RESOURCE xml:lang="en" type="get" id="0" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string" boolean="True">Info_inside_tag</example_tag></iq>
+
+ self.send_example_iq_with_inner_tag(self.to)
+ # <iq from="SENDER/RESOURCE" to="RESPONDER/RESOURCE" id="1" xml:lang="en" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ self.send_example_message(self.to)
+ # <message to="RESPONDER" xml:lang="en" from="SENDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" some_string="Message string">Info_inside_tag_message</example_tag></message>
+
+ self.send_example_iq_tag_from_file(self.to, self.path)
+ # <iq from="SENDER/RESOURCE" xml:lang="en" id="2" type="get" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag></iq>
+
+ string = '<example_tag xmlns="https://example.net/our_extension" some_string="Another_string">Info_inside_tag<inside_tag first_field="1" second_field="2" /></example_tag>'
+ et = ET.fromstring(string)
+ self.send_example_iq_tag_from_element_tree(self.to, et)
+ # <iq to="RESPONDER/RESOURCE" id="3" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+ self.send_example_iq_to_get_error(self.to)
+ # <iq type="get" id="4" from="SENDER/RESOURCE" xml:lang="en" to="RESPONDER/RESOURCE"><example_tag xmlns="https://example.net/our_extension" boolean="True" /></iq>
+ # OUR ERROR <iq to="RESPONDER/RESOURCE" id="4" xml:lang="en" from="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Without boolean value returns error.</text></error></iq>
+ # OFFLINE ERROR <iq id="4" from="RESPONDER/RESOURCE" xml:lang="en" to="SENDER/RESOURCE" type="error"><example_tag xmlns="https://example.net/our_extension" boolean="True" /><error type="cancel" code="503"><service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /><text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">User session not found</text></error></iq>
+
+ self.send_example_iq_tag_from_string(self.to, string)
+ # <iq to="RESPONDER/RESOURCE" id="5" xml:lang="en" from="SENDER/RESOURCE" type="get"><example_tag xmlns="https://example.net/our_extension" some_string="Reply_string" boolean="True">Info_inside_tag<inside_tag second_field="2" first_field="1" /></example_tag></iq>
+
+
+ def example_tag_result_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def example_tag_error_iq(self, iq):
+ self.disconnect_counter -= 1
+ logging.info(str(iq))
+ if not self.disconnect_counter:
+ self.disconnect() # Example disconnect after first received iq stanza extended by example_tag with result type.
+
+ def send_example_iq(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get")
+ iq['example_tag'].set_boolean(True)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+ iq.send()
+
+ def send_example_iq_with_inner_tag(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=1)
+ iq['example_tag'].set_some_string("Another_string")
+ iq['example_tag'].set_text("Info_inside_tag")
+
+ inner_attributes = {"first_field": "1", "second_field": "2"}
+ iq['example_tag'].add_inside_tag(tag="inside_tag", attributes=inner_attributes)
+
+ iq.send()
+
+ def send_example_message(self, to):
+ #~ make_message(mfrom=None, mto=None, mtype=None, mquery=None)
+ msg = self.make_message(mto=to)
+ msg['example_tag'].set_boolean(True)
+ msg['example_tag'].set_some_string("Message string")
+ msg['example_tag'].set_text("Info_inside_tag_message")
+ msg.send()
+
+ def send_example_iq_tag_from_file(self, to, path):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=2)
+ iq['example_tag'].setup_from_file(path)
+
+ iq.send()
+
+ def send_example_iq_tag_from_element_tree(self, to, et):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=3)
+ iq['example_tag'].setup_from_lxml(et)
+
+ iq.send()
+
+ def send_example_iq_to_get_error(self, to):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=4)
+ iq['example_tag'].set_boolean(True) # For example, the condition for receivingg error respond is example_tag without boolean value.
+ iq.send()
+
+ def send_example_iq_tag_from_string(self, to, string):
+ #~ make_iq(id=0, ifrom=None, ito=None, itype=None, iquery=None)
+ iq = self.make_iq(ito=to, itype="get", id=5)
+ iq['example_tag'].setup_from_string(string)
+
+ iq.send()
+
+ if __name__ == '__main__':
+ parser = ArgumentParser(description=Sender.__doc__)
+
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+ parser.add_argument("-t", "--to", dest="to",
+ help="JID to send the message/iq to")
+ parser.add_argument("--path", dest="path",
+ help="path to load example_tag content")
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel,
+ format=' %(name)s - %(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ xmpp = Sender(args.jid, args.password, args.to, args.path)
+ xmpp.register_plugin('OurPlugin', module=example_plugin) # OurPlugin is a class name from example_plugin
+
+ xmpp.connect()
+ try:
+ xmpp.process()
+ except KeyboardInterrupt:
+ try:
+ xmpp.disconnect()
+ except:
+ pass
+
+~
+
+.. code-block:: python
+
+ #File: $WORKDIR/test_example_tag.xml
+
+.. code-block:: xml
+
+ <example_tag xmlns="https://example.net/our_extension" some_string="StringFromFile">Info_inside_tag<inside_tag first_field="3" second_field="4" /></example_tag>
+
+Sources and references
+-----------------------
+
+The Slixmpp project description:
+
+* https://pypi.org/project/slixmpp/
+
+Official web documentation:
+
+* https://slixmpp.readthedocs.io/
+
+Official PDF documentation:
+
+* https://buildmedia.readthedocs.org/media/pdf/slixmpp/latest/slixmpp.pdf
+
+Note: Web and PDF Documentations have differences and some things are mentioned in only one of them.
diff --git a/docs/index.rst b/docs/index.rst
index 2f556d83..a18c77a7 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -21,7 +21,7 @@ Slixmpp
which goal is to use asyncio instead of threads to handle networking. See
:ref:`differences`.
-Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.5+,
+Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.7+,
Slixmpp's design goals and philosphy are:
diff --git a/examples/adhoc_provider.py b/examples/adhoc_provider.py
index 2bab2f46..d2c3afd6 100755
--- a/examples/adhoc_provider.py
+++ b/examples/adhoc_provider.py
@@ -33,7 +33,7 @@ class CommandBot(slixmpp.ClientXMPP):
# our roster.
self.add_event_handler("session_start", self.start)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -47,7 +47,7 @@ class CommandBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
# We add the command after session_start has fired
# to ensure that the correct full JID is used.
diff --git a/examples/adhoc_user.py b/examples/adhoc_user.py
index 8bdb675b..931ef71c 100755
--- a/examples/adhoc_user.py
+++ b/examples/adhoc_user.py
@@ -37,7 +37,7 @@ class CommandUserBot(slixmpp.ClientXMPP):
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -51,7 +51,7 @@ class CommandUserBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
# We first create a session dictionary containing:
# 'next' -- the handler to execute on a successful response
@@ -176,4 +176,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/admin_commands.py b/examples/admin_commands.py
index 72577f87..64c48913 100755
--- a/examples/admin_commands.py
+++ b/examples/admin_commands.py
@@ -30,7 +30,7 @@ class AdminCommands(slixmpp.ClientXMPP):
self.add_event_handler("session_start", self.start)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -44,7 +44,7 @@ class AdminCommands(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def command_success(iq, session):
print('Command completed')
diff --git a/examples/disco_browser.py b/examples/disco_browser.py
index f0ece32d..02d51259 100755
--- a/examples/disco_browser.py
+++ b/examples/disco_browser.py
@@ -69,7 +69,7 @@ class Disco(slixmpp.ClientXMPP):
event does not provide any additional
data.
"""
- self.get_roster()
+ await self.get_roster()
self.send_presence()
try:
diff --git a/examples/download_avatars.py b/examples/download_avatars.py
index 02591e3e..37733b01 100755
--- a/examples/download_avatars.py
+++ b/examples/download_avatars.py
@@ -159,4 +159,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/echo_client.py b/examples/echo_client.py
index 820ca014..2a3337a5 100755
--- a/examples/echo_client.py
+++ b/examples/echo_client.py
@@ -38,7 +38,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def message(self, msg):
"""
diff --git a/examples/gtalk_custom_domain.py b/examples/gtalk_custom_domain.py
index f055159b..c8d80e96 100755
--- a/examples/gtalk_custom_domain.py
+++ b/examples/gtalk_custom_domain.py
@@ -58,7 +58,7 @@ class GTalkBot(slixmpp.ClientXMPP):
logging.error(err.message)
self.disconnect()
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -72,7 +72,7 @@ class GTalkBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def message(self, msg):
"""
diff --git a/examples/markup.py b/examples/markup.py
index a72cbdb8..4c850ec8 100755
--- a/examples/markup.py
+++ b/examples/markup.py
@@ -39,7 +39,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -53,7 +53,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def message(self, msg):
"""
diff --git a/examples/migrate_roster.py b/examples/migrate_roster.py
index d599b10c..be457fb3 100755
--- a/examples/migrate_roster.py
+++ b/examples/migrate_roster.py
@@ -104,4 +104,4 @@ def on_session2(event):
new_xmpp.add_event_handler('session_start', on_session2)
new_xmpp.connect()
-new_xmpp.process()
+new_xmpp.process(forever=False)
diff --git a/examples/muc.py b/examples/muc.py
index 469e8f49..e3433b8f 100755
--- a/examples/muc.py
+++ b/examples/muc.py
@@ -52,7 +52,7 @@ class MUCBot(slixmpp.ClientXMPP):
self.muc_online)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -65,7 +65,7 @@ class MUCBot(slixmpp.ClientXMPP):
event does not provide any additional
data.
"""
- self.get_roster()
+ await self.get_roster()
self.send_presence()
self.plugin['xep_0045'].join_muc(self.room,
self.nick,
diff --git a/examples/ping.py b/examples/ping.py
index cb1bb968..7870715c 100755
--- a/examples/ping.py
+++ b/examples/ping.py
@@ -51,7 +51,7 @@ class PingTest(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
try:
rtt = await self['xep_0199'].ping(self.pingjid,
@@ -109,4 +109,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/proxy_echo_client.py b/examples/proxy_echo_client.py
index b149de31..566f5957 100755
--- a/examples/proxy_echo_client.py
+++ b/examples/proxy_echo_client.py
@@ -38,7 +38,7 @@ class EchoBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class EchoBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def message(self, msg):
"""
diff --git a/examples/pubsub_client.py b/examples/pubsub_client.py
index 480c7d89..e285dfc8 100755
--- a/examples/pubsub_client.py
+++ b/examples/pubsub_client.py
@@ -32,7 +32,7 @@ class PubsubClient(slixmpp.ClientXMPP):
self.add_event_handler('session_start', self.start)
async def start(self, event):
- self.get_roster()
+ await self.get_roster()
self.send_presence()
try:
diff --git a/examples/pubsub_events.py b/examples/pubsub_events.py
index e2fdc9cf..2b3a1e65 100755
--- a/examples/pubsub_events.py
+++ b/examples/pubsub_events.py
@@ -38,8 +38,8 @@ class PubsubEvents(slixmpp.ClientXMPP):
# self.add_event_handler('event_prefix_purge', handler)
# self.add_event_handler('event_prefix_delete', handler)
- def start(self, event):
- self.get_roster()
+ async def start(self, event):
+ await self.get_roster()
self.send_presence()
def _publish(self, msg):
diff --git a/examples/register_account.py b/examples/register_account.py
index 76b5fcfc..9ab0c664 100755
--- a/examples/register_account.py
+++ b/examples/register_account.py
@@ -47,7 +47,7 @@ class RegisterBot(slixmpp.ClientXMPP):
# for data forms and OOB links that will make that easier.
self.add_event_handler("register", self.register)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -61,7 +61,7 @@ class RegisterBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
# We're only concerned about registering, so nothing more to do here.
self.disconnect()
diff --git a/examples/roster_browser.py b/examples/roster_browser.py
index 6ad8b2a4..e9365d09 100755
--- a/examples/roster_browser.py
+++ b/examples/roster_browser.py
@@ -51,12 +51,8 @@ class RosterBrowser(slixmpp.ClientXMPP):
event does not provide any additional
data.
"""
- future = asyncio.Future()
- def callback(result):
- future.set_result(None)
try:
- self.get_roster(callback=callback)
- await future
+ await self.get_roster()
except IqError as err:
print('Error: %s' % err.iq['error']['condition'])
except IqTimeout:
@@ -138,4 +134,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/send_client.py b/examples/send_client.py
index 6e3e5865..5d5fb810 100755
--- a/examples/send_client.py
+++ b/examples/send_client.py
@@ -38,7 +38,7 @@ class SendMsgBot(slixmpp.ClientXMPP):
# our roster.
self.add_event_handler("session_start", self.start)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -52,7 +52,7 @@ class SendMsgBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
self.send_message(mto=self.recipient,
mbody=self.msg,
@@ -107,4 +107,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/set_avatar.py b/examples/set_avatar.py
index 6579b2e7..3188e9d8 100755
--- a/examples/set_avatar.py
+++ b/examples/set_avatar.py
@@ -46,7 +46,7 @@ class AvatarSetter(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
avatar_file = None
try:
@@ -137,4 +137,4 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
- xmpp.process()
+ xmpp.process(forever=False)
diff --git a/examples/thirdparty_auth.py b/examples/thirdparty_auth.py
index 4129fa91..b2623972 100755
--- a/examples/thirdparty_auth.py
+++ b/examples/thirdparty_auth.py
@@ -60,7 +60,7 @@ class ThirdPartyAuthBot(slixmpp.ClientXMPP):
# MUC messages and error messages.
self.add_event_handler("message", self.message)
- def start(self, event):
+ async def start(self, event):
"""
Process the session_start event.
@@ -74,7 +74,7 @@ class ThirdPartyAuthBot(slixmpp.ClientXMPP):
data.
"""
self.send_presence()
- self.get_roster()
+ await self.get_roster()
def message(self, msg):
"""
diff --git a/examples/user_location.py b/examples/user_location.py
index 7edbeabb..2d473678 100755
--- a/examples/user_location.py
+++ b/examples/user_location.py
@@ -38,9 +38,9 @@ class LocationBot(ClientXMPP):
self.current_tune = None
- def start(self, event):
+ async def start(self, event):
self.send_presence()
- self.get_roster()
+ await self.get_roster()
self['xep_0115'].update_caps()
print("Using freegeoip.net to get geolocation.")
diff --git a/examples/user_tune.py b/examples/user_tune.py
index 6aa0da8e..2ce29c1d 100755
--- a/examples/user_tune.py
+++ b/examples/user_tune.py
@@ -35,9 +35,9 @@ class TuneBot(ClientXMPP):
self.current_tune = None
- def start(self, event):
+ async def start(self, event):
self.send_presence()
- self.get_roster()
+ await self.get_roster()
self['xep_0115'].update_caps()
def _update_tune(self):
diff --git a/setup.py b/setup.py
index 8cab570e..9ba0b5df 100755
--- a/setup.py
+++ b/setup.py
@@ -28,9 +28,9 @@ CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -83,7 +83,7 @@ setup(
platforms=['any'],
packages=packages,
ext_modules=ext_modules,
- install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'aiohttp'],
+ install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'aiohttp', 'emoji'],
classifiers=CLASSIFIERS,
cmdclass={'test': TestCommand}
)
diff --git a/slixmpp/basexmpp.py b/slixmpp/basexmpp.py
index 146b6086..02c0b21c 100644
--- a/slixmpp/basexmpp.py
+++ b/slixmpp/basexmpp.py
@@ -111,6 +111,9 @@ class BaseXMPP(XMLStream):
#: outgoing messages an ID.
self.use_presence_ids = True
+ #: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
+ self.use_origin_id = True
+
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is
#: marked with:
diff --git a/slixmpp/jid.py b/slixmpp/jid.py
index cb9512b8..5d39a444 100644
--- a/slixmpp/jid.py
+++ b/slixmpp/jid.py
@@ -423,7 +423,10 @@ class JID:
if isinstance(other, UnescapedJID):
return False
if not isinstance(other, JID):
- other = JID(other)
+ try:
+ other = JID(other)
+ except InvalidJID:
+ return NotImplemented
return (self._node == other._node and
self._domain == other._domain and
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index d28cf281..a89b10f6 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -85,4 +85,7 @@ __all__ = [
'xep_0323', # IoT Systems Sensor Data
'xep_0325', # IoT Systems Control
'xep_0332', # HTTP Over XMPP Transport
+ 'xep_0377', # Spam reporting
+ 'xep_0421', # Anonymous unique occupant identifiers for MUCs
+ 'xep_0444', # Message Reactions
]
diff --git a/slixmpp/plugins/xep_0009/stanza/RPC.py b/slixmpp/plugins/xep_0009/stanza/RPC.py
index f8cec481..542c839c 100644
--- a/slixmpp/plugins/xep_0009/stanza/RPC.py
+++ b/slixmpp/plugins/xep_0009/stanza/RPC.py
@@ -7,7 +7,7 @@
"""
from slixmpp.xmlstream.stanzabase import ElementBase
-from xml.etree import cElementTree as ET
+from xml.etree import ElementTree as ET
class RPCQuery(ElementBase):
diff --git a/slixmpp/plugins/xep_0030/disco.py b/slixmpp/plugins/xep_0030/disco.py
index a8d0b75e..b79a14ff 100644
--- a/slixmpp/plugins/xep_0030/disco.py
+++ b/slixmpp/plugins/xep_0030/disco.py
@@ -300,6 +300,8 @@ class XEP_0030(BasePlugin):
async def get_info_from_domain(self, domain=None, timeout=None,
cached=True, callback=None):
+ """Fetch disco#info of specified domain and one disco#items level below"""
+
if domain is None:
domain = self.xmpp.boundjid.domain
diff --git a/slixmpp/plugins/xep_0045.py b/slixmpp/plugins/xep_0045.py
deleted file mode 100644
index 83521b01..00000000
--- a/slixmpp/plugins/xep_0045.py
+++ /dev/null
@@ -1,422 +0,0 @@
-"""
- Slixmpp: The Slick XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of Slixmpp.
-
- See the file LICENSE for copying permission.
-"""
-from __future__ import with_statement
-
-import logging
-
-from slixmpp import Presence, Message
-from slixmpp.plugins import BasePlugin, register_plugin
-from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
-from slixmpp.xmlstream.handler.callback import Callback
-from slixmpp.xmlstream.matcher.xpath import MatchXPath
-from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
-from slixmpp.exceptions import IqError, IqTimeout
-
-
-log = logging.getLogger(__name__)
-
-
-class MUCPresence(ElementBase):
- name = 'x'
- namespace = 'http://jabber.org/protocol/muc#user'
- plugin_attrib = 'muc'
- interfaces = {'affiliation', 'role', 'jid', 'nick', 'room'}
- affiliations = {'', }
- roles = {'', }
-
- def get_xml_item(self):
- item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
- if item is None:
- item = ET.Element('{http://jabber.org/protocol/muc#user}item')
- self.xml.append(item)
- return item
-
- def get_affiliation(self):
- #TODO if no affilation, set it to the default and return default
- item = self.get_xml_item()
- return item.get('affiliation', '')
-
- def set_affiliation(self, value):
- item = self.get_xml_item()
- #TODO check for valid affiliation
- item.attrib['affiliation'] = value
- return self
-
- def del_affiliation(self):
- item = self.get_xml_item()
- #TODO set default affiliation
- if 'affiliation' in item.attrib: del item.attrib['affiliation']
- return self
-
- def get_jid(self):
- item = self.get_xml_item()
- return JID(item.get('jid', ''))
-
- def set_jid(self, value):
- item = self.get_xml_item()
- if not isinstance(value, str):
- value = str(value)
- item.attrib['jid'] = value
- return self
-
- def del_jid(self):
- item = self.get_xml_item()
- if 'jid' in item.attrib: del item.attrib['jid']
- return self
-
- def get_role(self):
- item = self.get_xml_item()
- #TODO get default role, set default role if none
- return item.get('role', '')
-
- def set_role(self, value):
- item = self.get_xml_item()
- #TODO check for valid role
- item.attrib['role'] = value
- return self
-
- def del_role(self):
- item = self.get_xml_item()
- #TODO set default role
- if 'role' in item.attrib: del item.attrib['role']
- return self
-
- def get_nick(self):
- return self.parent()['from'].resource
-
- def get_room(self):
- return self.parent()['from'].bare
-
- def set_nick(self, value):
- log.warning("Cannot set nick through mucpresence plugin.")
- return self
-
- def set_room(self, value):
- log.warning("Cannot set room through mucpresence plugin.")
- return self
-
- def del_nick(self):
- log.warning("Cannot delete nick through mucpresence plugin.")
- return self
-
- def del_room(self):
- log.warning("Cannot delete room through mucpresence plugin.")
- return self
-
-
-class XEP_0045(BasePlugin):
-
- """
- Implements XEP-0045 Multi-User Chat
- """
-
- name = 'xep_0045'
- description = 'XEP-0045: Multi-User Chat'
- dependencies = {'xep_0030', 'xep_0004'}
-
- def plugin_init(self):
- self.rooms = {}
- self.our_nicks = {}
- self.xep = '0045'
- # load MUC support in presence stanzas
- register_stanza_plugin(Presence, MUCPresence)
- self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
- self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message))
- self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
- self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
- self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
- self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
- self.xmpp.default_ns,
- 'http://jabber.org/protocol/muc#user',
- 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
-
- def plugin_end(self):
- self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc')
-
- def session_bind(self, jid):
- self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc')
-
- def handle_groupchat_invite(self, inv):
- """ Handle an invite into a muc.
- """
- logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
- if inv['from'] not in self.rooms.keys():
- self.xmpp.event("groupchat_invite", inv)
-
- def handle_config_change(self, msg):
- """Handle a MUC configuration change (with status code)."""
- self.xmpp.event('groupchat_config_status', msg)
- self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
-
- def handle_groupchat_presence(self, pr):
- """ Handle a presence in a muc.
- """
- got_offline = False
- got_online = False
- if pr['muc']['room'] not in self.rooms.keys():
- return
- self.xmpp.roster[pr['from']].ignore_updates = True
- entry = pr['muc'].get_stanza_values()
- entry['show'] = pr['show']
- entry['status'] = pr['status']
- entry['alt_nick'] = pr['nick']
- if pr['type'] == 'unavailable':
- if entry['nick'] in self.rooms[entry['room']]:
- del self.rooms[entry['room']][entry['nick']]
- got_offline = True
- else:
- if entry['nick'] not in self.rooms[entry['room']]:
- got_online = True
- self.rooms[entry['room']][entry['nick']] = entry
- log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
- self.xmpp.event("groupchat_presence", pr)
- self.xmpp.event("muc::%s::presence" % entry['room'], pr)
- if got_offline:
- self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
- if got_online:
- self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
-
- def handle_groupchat_message(self, msg: Message) -> None:
- """ Handle a message event in a muc.
- """
- self.xmpp.event('groupchat_message', msg)
- self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
-
- def handle_groupchat_error_message(self, msg):
- """ Handle a message error event in a muc.
- """
- self.xmpp.event('groupchat_message_error', msg)
- self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
-
-
-
- def handle_groupchat_subject(self, msg: Message) -> None:
- """ Handle a message coming from a muc indicating
- a change of subject (or announcing it when joining the room)
- """
- # See poezio#3452. A message containing subject _and_ (body or thread)
- # is not a subject change.
- if msg['body'] or msg['thread']:
- return None
- self.xmpp.event('groupchat_subject', msg)
-
- def jid_in_room(self, room, jid):
- for nick in self.rooms[room]:
- entry = self.rooms[room][nick]
- if entry is not None and entry['jid'].full == jid:
- return True
- return False
-
- def get_nick(self, room, jid):
- for nick in self.rooms[room]:
- entry = self.rooms[room][nick]
- if entry is not None and entry['jid'].full == jid:
- return nick
-
- def configure_room(self, room, form=None, ifrom=None):
- if form is None:
- form = self.get_room_config(room, ifrom=ifrom)
- iq = self.xmpp.make_iq_set()
- iq['to'] = room
- if ifrom is not None:
- iq['from'] = ifrom
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- form['type'] = 'submit'
- query.append(form)
- iq.append(query)
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def join_muc(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
- """ Join the specified room, requesting 'maxhistory' lines of history.
- """
- stanza = self.xmpp.make_presence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
- x = ET.Element('{http://jabber.org/protocol/muc}x')
- if password:
- passelement = ET.Element('{http://jabber.org/protocol/muc}password')
- passelement.text = password
- x.append(passelement)
- if maxhistory:
- history = ET.Element('{http://jabber.org/protocol/muc}history')
- if maxhistory == "0":
- history.attrib['maxchars'] = maxhistory
- else:
- history.attrib['maxstanzas'] = maxhistory
- x.append(history)
- stanza.append(x)
- if not wait:
- self.xmpp.send(stanza)
- else:
- #wait for our own room presence back
- expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
- self.xmpp.send(stanza, expect)
- self.rooms[room] = {}
- self.our_nicks[room] = nick
-
- def destroy(self, room, reason='', altroom = '', ifrom=None):
- iq = self.xmpp.make_iq_set()
- if ifrom is not None:
- iq['from'] = ifrom
- iq['to'] = room
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
- if altroom:
- destroy.attrib['jid'] = altroom
- xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
- xreason.text = reason
- destroy.append(xreason)
- query.append(destroy)
- iq.append(query)
- # For now, swallow errors to preserve existing API
- try:
- r = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def set_affiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
- """ Change room affiliation."""
- if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- if nick is not None:
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
- else:
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
- query.append(item)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- return True
-
- def set_role(self, room, nick, role):
- """ Change role property of a nick in a room.
- Typically, roles are temporary (they last only as long as you are in the
- room), whereas affiliations are permanent (they last across groupchat
- sessions).
- """
- if role not in ('moderator', 'participant', 'visitor', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- item = ET.Element('item', {'role':role, 'nick':nick})
- query.append(item)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- result = iq.send()
- if result is False or result['type'] != 'result':
- raise ValueError
- return True
-
- def invite(self, room, jid, reason='', mfrom=''):
- """ Invite a jid to a room."""
- msg = self.xmpp.make_message(room)
- msg['from'] = mfrom
- x = ET.Element('{http://jabber.org/protocol/muc#user}x')
- invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
- if reason:
- rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
- rxml.text = reason
- invite.append(rxml)
- x.append(invite)
- msg.append(x)
- self.xmpp.send(msg)
-
- def leave_muc(self, room, nick, msg='', pfrom=None):
- """ Leave the specified room.
- """
- if msg:
- self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
- else:
- self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
- del self.rooms[room]
-
- def get_room_config(self, room, ifrom=''):
- iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner')
- iq['to'] = room
- iq['from'] = ifrom
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- raise ValueError
- except IqTimeout:
- raise ValueError
- form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
- if form is None:
- raise ValueError
- return self.xmpp.plugin['xep_0004'].build_form(form)
-
- def cancel_config(self, room, ifrom=None):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- x = ET.Element('{jabber:x:data}x', type='cancel')
- query.append(x)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- iq.send()
-
- def set_room_config(self, room, config, ifrom=''):
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- config['type'] = 'submit'
- query.append(config)
- iq = self.xmpp.make_iq_set(query)
- iq['to'] = room
- iq['from'] = ifrom
- iq.send()
-
- def get_joined_rooms(self):
- return self.rooms.keys()
-
- def get_our_jid_in_room(self, room_jid):
- """ Return the jid we're using in a room.
- """
- return "%s/%s" % (room_jid, self.our_nicks[room_jid])
-
- def get_jid_property(self, room, nick, jid_property):
- """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
- If not found, return None.
- """
- if room in self.rooms and nick in self.rooms[room] and jid_property in self.rooms[room][nick]:
- return self.rooms[room][nick][jid_property]
- else:
- return None
-
- def get_roster(self, room):
- """ Get the list of nicks in a room.
- """
- if room not in self.rooms.keys():
- return None
- return self.rooms[room].keys()
-
- def get_users_by_affiliation(cls, room, affiliation='member', ifrom=None):
- if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
- raise TypeError
- query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
- item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
- query.append(item)
- iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
- iq.append(query)
- return iq.send()
-
-
-register_plugin(XEP_0045)
diff --git a/slixmpp/plugins/xep_0045/__init__.py b/slixmpp/plugins/xep_0045/__init__.py
new file mode 100644
index 00000000..eb13b018
--- /dev/null
+++ b/slixmpp/plugins/xep_0045/__init__.py
@@ -0,0 +1,14 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins import register_plugin
+from slixmpp.plugins.xep_0045 import stanza
+from slixmpp.plugins.xep_0045.muc import XEP_0045
+from slixmpp.plugins.xep_0045.stanza import MUCPresence, MUCMessage
+
+register_plugin(XEP_0045)
diff --git a/slixmpp/plugins/xep_0045/muc.py b/slixmpp/plugins/xep_0045/muc.py
new file mode 100644
index 00000000..f310c03e
--- /dev/null
+++ b/slixmpp/plugins/xep_0045/muc.py
@@ -0,0 +1,382 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+from __future__ import with_statement
+
+import logging
+from typing import (
+ List,
+ Tuple,
+ Optional,
+)
+
+from slixmpp import (
+ Presence,
+ Message,
+ Iq,
+ JID,
+)
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin, ET
+from slixmpp.xmlstream.handler.callback import Callback
+from slixmpp.xmlstream.matcher.xpath import MatchXPath
+from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from slixmpp.exceptions import IqError, IqTimeout
+
+from slixmpp.plugins.xep_0045 import stanza
+from slixmpp.plugins.xep_0045.stanza import (
+ MUCPresence,
+ MUCJoin,
+ MUCMessage,
+ MUCAdminQuery,
+ MUCAdminItem,
+ MUCHistory,
+ MUCOwnerQuery,
+ MUCOwnerDestroy,
+)
+
+
+log = logging.getLogger(__name__)
+
+AFFILIATIONS = ('outcast', 'member', 'admin', 'owner', 'none')
+ROLES = ('moderator', 'participant', 'visitor', 'none')
+
+
+class XEP_0045(BasePlugin):
+
+ """
+ Implements XEP-0045 Multi-User Chat
+ """
+
+ name = 'xep_0045'
+ description = 'XEP-0045: Multi-User Chat'
+ dependencies = {'xep_0030', 'xep_0004'}
+ stanza = stanza
+
+ def plugin_init(self):
+ self.rooms = {}
+ self.our_nicks = {}
+ # load MUC support in presence stanzas
+ register_stanza_plugin(Presence, MUCPresence)
+ register_stanza_plugin(Presence, MUCJoin)
+ register_stanza_plugin(MUCJoin, MUCHistory)
+ register_stanza_plugin(Message, MUCMessage)
+ register_stanza_plugin(Iq, MUCAdminQuery)
+ register_stanza_plugin(Iq, MUCOwnerQuery)
+ register_stanza_plugin(MUCOwnerQuery, MUCOwnerDestroy)
+ register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True)
+
+ # Register handlers
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCPresence',
+ MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns),
+ self.handle_groupchat_presence,
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCError',
+ MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_error_message
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCMessage',
+ MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_message
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCSubject',
+ MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns),
+ self.handle_groupchat_subject
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCConfig',
+ MatchXMLMask(
+ "<message xmlns='%s' type='groupchat'>"
+ "<x xmlns='http://jabber.org/protocol/muc#user'><status/></x>"
+ "</message>" % self.xmpp.default_ns
+ ),
+ self.handle_config_change
+ ))
+ self.xmpp.register_handler(
+ Callback(
+ 'MUCInvite',
+ MatchXPath("{%s}message/{%s}x/{%s}invite" % (
+ self.xmpp.default_ns,
+ stanza.NS_USER,
+ stanza.NS_USER
+ )),
+ self.handle_groupchat_invite
+ ))
+
+ def plugin_end(self):
+ self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS)
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature(stanza.NS)
+
+ def handle_groupchat_invite(self, inv):
+ """ Handle an invite into a muc.
+ """
+ logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
+ if inv['from'] not in self.rooms.keys():
+ self.xmpp.event("groupchat_invite", inv)
+
+ def handle_config_change(self, msg):
+ """Handle a MUC configuration change (with status code)."""
+ self.xmpp.event('groupchat_config_status', msg)
+ self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
+
+ def handle_groupchat_presence(self, pr):
+ """ Handle a presence in a muc.
+ """
+ got_offline = False
+ got_online = False
+ if pr['muc']['room'] not in self.rooms.keys():
+ return
+ self.xmpp.roster[pr['from']].ignore_updates = True
+ entry = pr['muc'].get_stanza_values()
+ entry['show'] = pr['show'] if pr['show'] in pr.showtypes else None
+ entry['status'] = pr['status']
+ entry['alt_nick'] = pr['nick']
+ if pr['type'] == 'unavailable':
+ if entry['nick'] in self.rooms[entry['room']]:
+ del self.rooms[entry['room']][entry['nick']]
+ got_offline = True
+ else:
+ if entry['nick'] not in self.rooms[entry['room']]:
+ got_online = True
+ self.rooms[entry['room']][entry['nick']] = entry
+ log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
+ self.xmpp.event("groupchat_presence", pr)
+ self.xmpp.event("muc::%s::presence" % entry['room'], pr)
+ if got_offline:
+ self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
+ if got_online:
+ self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
+
+ def handle_groupchat_message(self, msg: Message) -> None:
+ """ Handle a message event in a muc.
+ """
+ self.xmpp.event('groupchat_message', msg)
+ self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+
+ def handle_groupchat_error_message(self, msg):
+ """ Handle a message error event in a muc.
+ """
+ self.xmpp.event('groupchat_message_error', msg)
+ self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
+
+
+ def handle_groupchat_subject(self, msg: Message) -> None:
+ """ Handle a message coming from a muc indicating
+ a change of subject (or announcing it when joining the room)
+ """
+ # See poezio#3452. A message containing subject _and_ (body or thread)
+ # is not a subject change.
+ if msg['body'] or msg['thread']:
+ return None
+ self.xmpp.event('groupchat_subject', msg)
+
+ def jid_in_room(self, room: JID, jid: JID) -> bool:
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return True
+ return False
+
+ def get_nick(self, room: JID, jid: JID) -> Optional[str]:
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return nick
+
+ def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
+ pstatus='', pshow='', pfrom=''):
+ """ Join the specified room, requesting 'maxhistory' lines of history.
+ """
+ stanza = self.xmpp.make_presence(
+ pto="%s/%s" % (room, nick), pstatus=pstatus,
+ pshow=pshow, pfrom=pfrom
+ )
+ stanza.enable('muc_join')
+ if password:
+ stanza['muc_join']['password'] = password
+ if maxhistory:
+ if maxhistory == "0":
+ stanza['muc_join']['history']['maxchars'] = '0'
+ else:
+ stanza['muc_join']['history']['maxstanzas'] = str(maxhistory)
+ self.xmpp.send(stanza)
+ self.rooms[room] = {}
+ self.our_nicks[room] = nick
+
+ async def destroy(self, room: JID, reason='', altroom='', *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ iq = self.xmpp.make_iq_set(ifrom=ifrom, ito=room)
+ iq.enable('mucowner_query')
+ iq['mucowner_query'].enable('destroy')
+ if altroom:
+ iq['mucowner_query']['destroy']['jid'] = altroom
+ if reason:
+ iq['mucowner_query']['destroy']['reason'] = reason
+ await iq.send(**iqkwargs)
+
+ async def set_affiliation(self, room: JID, jid: Optional[JID] = None, nick: Optional[str] = None, *, affiliation: str,
+ ifrom: Optional[JID] = None, **iqkwargs):
+ """ Change room affiliation."""
+ if affiliation not in AFFILIATIONS:
+ raise ValueError('%s is not a valid affiliation' % affiliation)
+ if not any((jid, nick)):
+ raise ValueError('One of jid or nick must be set')
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ iq.enable('mucadmin_query')
+ item = MUCAdminItem()
+ item['affiliation'] = affiliation
+ if nick:
+ item['nick'] = nick
+ if jid:
+ item['jid'] = jid
+ iq['mucadmin_query'].append(item)
+ await iq.send(**iqkwargs)
+
+ async def set_role(self, room: JID, nick: str, role: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """ Change role property of a nick in a room.
+ Typically, roles are temporary (they last only as long as you are in the
+ room), whereas affiliations are permanent (they last across groupchat
+ sessions).
+ """
+ if role not in ROLES:
+ raise ValueError("Role %s does not exist" % role)
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ iq.enable('mucadmin_query')
+ item = MUCAdminItem()
+ item['role'] = role
+ item['nick'] = nick
+ iq['mucadmin_query'].append(item)
+ await iq.send(**iqkwargs)
+
+ def invite(self, room: JID, jid: JID, reason='', *,
+ mfrom: Optional[JID] = None):
+ """ Invite a jid to a room."""
+ msg = self.xmpp.make_message(room, mfrom=mfrom)
+ msg.enable('muc')
+ msg['muc']['invite'] = jid
+ if reason:
+ msg['muc']['invite']['reason'] = reason
+ self.xmpp.send(msg)
+
+ def leave_muc(self, room: JID, nick: str, msg='', pfrom=None):
+ """ Leave the specified room.
+ """
+ if msg:
+ self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
+ else:
+ self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
+ del self.rooms[room]
+
+
+ async def get_room_config(self, room: JID, ifrom=''):
+ """Get the room config form in 0004 plugin format """
+ iq = self.xmpp.make_iq_get(stanza.NS_OWNER, ito=room, ifrom=ifrom)
+ # For now, swallow errors to preserve existing API
+ result = await iq.send()
+ form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if form is None:
+ raise ValueError("Configuration form not found")
+ return self.xmpp.plugin['xep_0004'].build_form(form)
+
+ async def cancel_config(self, room: JID, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Cancel a requested config form"""
+ query = MUCOwnerQuery()
+ x = ET.Element('{jabber:x:data}x', type='cancel')
+ query.append(x)
+ iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
+ return await iq.send(**iqkwargs)
+
+ async def set_room_config(self, room: JID, config, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Send a room config form"""
+ query = MUCOwnerQuery()
+ config['type'] = 'submit'
+ query.append(config)
+ iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
+ return await iq.send(**iqkwargs)
+
+ async def get_affiliation_list(self, room: JID, affiliation: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> List[JID]:
+ """"Get a list of JIDs with the specified affiliation"""
+ iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
+ iq['mucadmin_query']['item']['affiliation'] = affiliation
+ result = await iq.send(**iqkwargs)
+ return [item['jid'] for item in result['mucadmin_query']]
+
+ async def get_roles_list(self, room: JID, role: str, *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> List[str]:
+ """"Get a list of JIDs with the specified role"""
+ iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
+ iq['mucadmin_query']['item']['role'] = role
+ result = await iq.send(**iqkwargs)
+ return [item['nick'] for item in result['mucadmin_query']]
+
+ async def send_affiliation_list(self, room: JID, affiliations: List[Tuple[JID, str]], *,
+ ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
+ """Send an affiliation delta list"""
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ for jid, affiliation in affiliations:
+ item = MUCAdminItem()
+ item['jid'] = jid
+ item['affiliation'] = affiliation
+ iq['mucadmin_query'].append(item)
+ return await iq.send(**iqkwargs)
+
+ async def send_role_list(self, room: JID, roles: List[Tuple[str, str]], *,
+ ifrom: Optional[JID], **iqkwargs) -> Iq:
+ """Send a role delta list"""
+ iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
+ for nick, affiliation in roles:
+ item = MUCAdminItem()
+ item['nick'] = nick
+ item['affiliation'] = affiliation
+ iq['mucadmin_query'].append(item)
+ return await iq.send(**iqkwargs)
+
+ def get_joined_rooms(self) -> List[JID]:
+ return self.rooms.keys()
+
+ def get_our_jid_in_room(self, room_jid: JID) -> str:
+ """ Return the jid we're using in a room.
+ """
+ return "%s/%s" % (room_jid, self.our_nicks[room_jid])
+
+ def get_jid_property(self, room, nick, jid_property):
+ """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
+ If not found, return None.
+ """
+ if room in self.rooms and nick in self.rooms[room] and jid_property in self.rooms[room][nick]:
+ return self.rooms[room][nick][jid_property]
+ else:
+ return None
+
+ def get_roster(self, room: JID) -> List[str]:
+ """ Get the list of nicks in a room.
+ """
+ if room not in self.rooms.keys():
+ return None
+ return self.rooms[room].keys()
+
+ def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
+ # Preserve old API
+ if affiliation not in AFFILIATIONS:
+ raise TypeError
+ return self.get_affiliation_list(room, affiliation, ifrom=ifrom)
diff --git a/slixmpp/plugins/xep_0045/stanza.py b/slixmpp/plugins/xep_0045/stanza.py
new file mode 100644
index 00000000..9756790b
--- /dev/null
+++ b/slixmpp/plugins/xep_0045/stanza.py
@@ -0,0 +1,198 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from slixmpp.xmlstream import ElementBase, ET, JID
+
+
+log = logging.getLogger(__name__)
+
+NS = 'http://jabber.org/protocol/muc'
+NS_USER = 'http://jabber.org/protocol/muc#user'
+NS_ADMIN = 'http://jabber.org/protocol/muc#admin'
+NS_OWNER = 'http://jabber.org/protocol/muc#owner'
+
+
+class MUCBase(ElementBase):
+ name = 'x'
+ namespace = NS_USER
+ plugin_attrib = 'muc'
+ interfaces = {'affiliation', 'role', 'jid', 'nick', 'room'}
+
+ def get_item_attr(self, attr, default: str):
+ item = self.xml.find(f'{{{NS_USER}}}item')
+ if item is None:
+ return default
+ return item.get(attr)
+
+ def set_item_attr(self, attr, value: str):
+ item = self.xml.find(f'{{{NS_USER}}}item')
+ if item is None:
+ item = ET.Element(f'{{{NS_USER}}}item')
+ self.xml.append(item)
+ item.attrib[attr] = value
+ return item
+
+ def del_item_attr(self, attr):
+ item = self.xml.find(f'{{{NS_USER}}}item')
+ if item is not None and attr in item.attrib:
+ del item.attrib[attr]
+
+ def get_affiliation(self):
+ return self.get_item_attr('affiliation', '')
+
+ def set_affiliation(self, value):
+ self.set_item_attr('affiliation', value)
+ return self
+
+ def del_affiliation(self):
+ # TODO: set default affiliation
+ self.del_item_attr('affiliation')
+ return self
+
+ def get_jid(self):
+ return JID(self.get_item_attr('jid', ''))
+
+ def set_jid(self, value):
+ if not isinstance(value, str):
+ value = str(value)
+ self.set_item_attr('jid', value)
+ return self
+
+ def del_jid(self):
+ self.del_item_attr('jid')
+ return self
+
+ def get_role(self):
+ return self.get_item_attr('role', '')
+
+ def set_role(self, value):
+ # TODO: check for valid role
+ self.set_item_attr('role', value)
+ return self
+
+ def del_role(self):
+ # TODO: set default role
+ self.del_item_attr('role')
+ return self
+
+ def get_nick(self):
+ return self.parent()['from'].resource
+
+ def get_room(self):
+ return self.parent()['from'].bare
+
+ def set_nick(self, value):
+ log.warning(
+ "Cannot set nick through the %s plugin.",
+ self.__class__.__name__,
+ )
+ return self
+
+ def set_room(self, value):
+ log.warning(
+ "Cannot set room through the %s plugin.",
+ self.__class__.__name__,
+ )
+ return self
+
+ def del_nick(self):
+ log.warning(
+ "Cannot delete nick through the %s plugin.",
+ self.__class__.__name__,
+ )
+ return self
+
+ def del_room(self):
+ log.warning(
+ "Cannot delete room through the %s plugin.",
+ self.__class__.__name__,
+ )
+ return self
+
+
+class MUCPresence(MUCBase):
+ '''
+ A MUC Presence
+
+ <presence from='foo@muc/user1' type='unavailable'>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='none'
+ role='none'
+ nick='newnick2'
+ jid='some@jid'/>
+ <status code='303'/>
+ </x>
+ </presence>
+ '''
+
+
+class MUCMessage(MUCBase):
+ '''
+ A MUC Message
+
+ <message from='foo@muc/user1' type='groupchat' id='someid'>
+ <body>Foo</body>
+ <x xmlns='http://jabber.org/protocol/muc#user'>
+ <item affiliation='none'
+ role='none'
+ nick='newnick2'
+ jid='some@jid'/>
+ </x>
+ </message>
+ '''
+
+class MUCJoin(ElementBase):
+ name = 'x'
+ namespace = NS
+ plugin_attrib = 'muc_join'
+ interfaces = {'password'}
+ sub_interfaces = {'password'}
+
+
+class MUCInvite(ElementBase):
+ name = 'invite'
+ plugin_attrib = 'invite'
+ namespace = NS_USER
+ interfaces = {'to', 'reason'}
+ sub_interfaces = {'reason'}
+
+
+class MUCHistory(ElementBase):
+ name = 'history'
+ plugin_attrib = 'history'
+ namespace = NS
+ interfaces = {'maxchars', 'maxstanzas', 'since', 'seconds'}
+
+
+class MUCOwnerQuery(ElementBase):
+ name = 'query'
+ plugin_attrib = 'mucowner_query'
+ namespace = NS_OWNER
+
+
+class MUCOwnerDestroy(ElementBase):
+ name = 'destroy'
+ plugin_attrib = 'destroy'
+ interfaces = {'reason', 'jid'}
+ sub_interfaces = {'reason'}
+
+
+class MUCAdminQuery(ElementBase):
+ name = 'query'
+ plugin_attrib = 'mucadmin_query'
+ namespace = NS_ADMIN
+
+
+class MUCAdminItem(ElementBase):
+ namespace = NS_ADMIN
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = {'role', 'affiliation', 'nick', 'jid'}
+
diff --git a/slixmpp/plugins/xep_0048/stanza.py b/slixmpp/plugins/xep_0048/stanza.py
index ddcc8841..7a2af4ee 100644
--- a/slixmpp/plugins/xep_0048/stanza.py
+++ b/slixmpp/plugins/xep_0048/stanza.py
@@ -6,6 +6,7 @@
See the file LICENSE for copying permission.
"""
+from slixmpp import JID
from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
@@ -52,6 +53,12 @@ class Conference(ElementBase):
if value in ('1', 'true', True):
self._set_attr('autojoin', 'true')
+ def set_jid(self, value):
+ del self['jid']
+ if isinstance(value, JID):
+ value = value.full
+ self._set_attr('jid', value)
+
class URL(ElementBase):
name = 'url'
diff --git a/slixmpp/plugins/xep_0059/rsm.py b/slixmpp/plugins/xep_0059/rsm.py
index a8eeea60..a7aaa7ce 100644
--- a/slixmpp/plugins/xep_0059/rsm.py
+++ b/slixmpp/plugins/xep_0059/rsm.py
@@ -79,7 +79,8 @@ class ResultIterator:
"""
if self._stop:
raise StopAsyncIteration
- self.query[self.interface]['rsm']['before'] = self.reverse
+ if self.query[self.interface]['rsm']['before'] is None:
+ self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -141,7 +142,7 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace)
- def iterate(self, stanza, interface, results='substanzas',
+ def iterate(self, stanza, interface, results='substanzas', amount=10, reverse=False,
recv_interface=None, pre_cb=None, post_cb=None):
"""
Create a new result set iterator for a given stanza query.
@@ -169,6 +170,6 @@ class XEP_0059(BasePlugin):
results -- The name of the interface containing the
query results (typically just 'substanzas').
"""
- return ResultIterator(stanza, interface, results,
+ return ResultIterator(stanza, interface, results, amount, reverse=reverse,
recv_interface=recv_interface, pre_cb=pre_cb,
post_cb=post_cb)
diff --git a/slixmpp/plugins/xep_0115/caps.py b/slixmpp/plugins/xep_0115/caps.py
index 749b74bd..0acfa83a 100644
--- a/slixmpp/plugins/xep_0115/caps.py
+++ b/slixmpp/plugins/xep_0115/caps.py
@@ -157,10 +157,12 @@ class XEP_0115(BasePlugin):
self.assign_verstring(pres['from'], ver)
return
+ ifrom = pres['to'] if self.xmpp.is_component else None
+
if pres['caps']['hash'] not in self.hashes:
try:
log.debug("Unknown caps hash: %s", pres['caps']['hash'])
- self.xmpp['xep_0030'].get_info(jid=pres['from'])
+ self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom)
return
except XMPPError:
return
@@ -169,7 +171,8 @@ class XEP_0115(BasePlugin):
try:
node = '%s#%s' % (pres['caps']['node'], ver)
caps = await self.xmpp['xep_0030'].get_info(pres['from'], node,
- coroutine=True)
+ coroutine=True,
+ ifrom=ifrom)
if isinstance(caps, Iq):
caps = caps['disco_info']
diff --git a/slixmpp/plugins/xep_0196/stanza.py b/slixmpp/plugins/xep_0196/stanza.py
index 79f5621e..756208b2 100644
--- a/slixmpp/plugins/xep_0196/stanza.py
+++ b/slixmpp/plugins/xep_0196/stanza.py
@@ -11,10 +11,9 @@ from slixmpp.xmlstream import ElementBase, ET
class UserGaming(ElementBase):
- name = 'gaming'
+ name = 'game'
namespace = 'urn:xmpp:gaming:0'
plugin_attrib = 'gaming'
interfaces = {'character_name', 'character_profile', 'name',
'level', 'server_address', 'server_name', 'uri'}
sub_interfaces = interfaces
-
diff --git a/slixmpp/plugins/xep_0198/stream_management.py b/slixmpp/plugins/xep_0198/stream_management.py
index 759e82e1..0200646a 100644
--- a/slixmpp/plugins/xep_0198/stream_management.py
+++ b/slixmpp/plugins/xep_0198/stream_management.py
@@ -71,7 +71,8 @@ class XEP_0198(BasePlugin):
self.window_counter = self.window
- self.enabled = False
+ self.enabled_in = False
+ self.enabled_out = False
self.unacked_queue = collections.deque()
register_stanza_plugin(StreamFeatures, stanza.StreamManagement)
@@ -82,10 +83,6 @@ class XEP_0198(BasePlugin):
self.xmpp.register_stanza(stanza.Ack)
self.xmpp.register_stanza(stanza.RequestAck)
- # Only end the session when a </stream> element is sent,
- # not just because the connection has died.
- self.xmpp.end_session_on_disconnect = False
-
# Register the feature twice because it may be ordered two
# different ways: enabling after binding and resumption
# before binding.
@@ -131,6 +128,7 @@ class XEP_0198(BasePlugin):
self.xmpp.add_filter('in', self._handle_incoming)
self.xmpp.add_filter('out_sync', self._handle_outgoing)
+ self.xmpp.add_event_handler('disconnected', self.disconnected)
self.xmpp.add_event_handler('session_end', self.session_end)
def plugin_end(self):
@@ -139,6 +137,7 @@ class XEP_0198(BasePlugin):
self.xmpp.unregister_feature('sm', self.order)
self.xmpp.unregister_feature('sm', self.resume_order)
+ self.xmpp.del_event_handler('disconnected', self.disconnected)
self.xmpp.del_event_handler('session_end', self.session_end)
self.xmpp.del_filter('in', self._handle_incoming)
self.xmpp.del_filter('out_sync', self._handle_outgoing)
@@ -154,9 +153,19 @@ class XEP_0198(BasePlugin):
self.xmpp.remove_stanza(stanza.Ack)
self.xmpp.remove_stanza(stanza.RequestAck)
+ def disconnected(self, event):
+ """Reset enabled state until we can resume/reenable."""
+ log.debug("disconnected, disabling SM")
+ self.xmpp.event('sm_disabled', event)
+ self.enabled_in = False
+ self.enabled_out = False
+
def session_end(self, event):
"""Reset stream management state."""
- self.enabled = False
+ log.debug("session_end, disabling SM")
+ self.xmpp.event('sm_disabled', event)
+ self.enabled_in = False
+ self.enabled_out = False
self.unacked_queue.clear()
self.sm_id = None
self.handled = 0
@@ -171,6 +180,7 @@ class XEP_0198(BasePlugin):
def request_ack(self, e=None):
"""Request an ack from the server."""
+ log.debug("requesting ack")
req = stanza.RequestAck(self.xmpp)
self.xmpp.send_raw(str(req))
@@ -193,9 +203,7 @@ class XEP_0198(BasePlugin):
enable = stanza.Enable(self.xmpp)
enable['resume'] = self.allow_resume
enable.send()
- self.enabled = True
- self.handled = 0
- self.unacked_queue.clear()
+ log.debug("enabling SM")
waiter = Waiter('enabled_or_failed',
MatchMany([
@@ -204,11 +212,11 @@ class XEP_0198(BasePlugin):
self.xmpp.register_handler(waiter)
result = await waiter.wait()
elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
- self.enabled = True
resume = stanza.Resume(self.xmpp)
resume['h'] = self.handled
resume['previd'] = self.sm_id
resume.send()
+ log.debug("resuming SM")
# Wait for a response before allowing stream feature processing
# to continue. The actual result processing will be done in the
@@ -231,7 +239,10 @@ class XEP_0198(BasePlugin):
self.xmpp.features.add('stream_management')
if stanza['id']:
self.sm_id = stanza['id']
+ self.enabled_in = True
+ self.handled = 0
self.xmpp.event('sm_enabled', stanza)
+ self.xmpp.end_session_on_disconnect = False
def _handle_resumed(self, stanza):
"""Finish resuming a stream by resending unacked stanzas.
@@ -239,10 +250,12 @@ class XEP_0198(BasePlugin):
Raises a :term:`session_resumed` event.
"""
self.xmpp.features.add('stream_management')
+ self.enabled_in = True
self._handle_ack(stanza)
for id, stanza in self.unacked_queue:
self.xmpp.send(stanza, use_filters=False)
self.xmpp.event('session_resumed', stanza)
+ self.xmpp.end_session_on_disconnect = False
def _handle_failed(self, stanza):
"""
@@ -252,7 +265,8 @@ class XEP_0198(BasePlugin):
Raises an :term:`sm_failed` event.
"""
- self.enabled = False
+ self.enabled_in = False
+ self.enabled_out = False
self.unacked_queue.clear()
self.xmpp.event('sm_failed', stanza)
@@ -289,7 +303,7 @@ class XEP_0198(BasePlugin):
def _handle_incoming(self, stanza):
"""Increment the handled counter for each inbound stanza."""
- if not self.enabled:
+ if not self.enabled_in:
return stanza
if isinstance(stanza, (Message, Presence, Iq)):
@@ -299,7 +313,13 @@ class XEP_0198(BasePlugin):
def _handle_outgoing(self, stanza):
"""Store outgoing stanzas in a queue to be acked."""
- if not self.enabled:
+ from slixmpp.plugins.xep_0198 import stanza as st
+ if isinstance(stanza, (st.Enable, st.Resume)):
+ self.enabled_out = True
+ self.unacked_queue.clear()
+ log.debug("enabling outgoing SM: %s" % stanza)
+
+ if not self.enabled_out:
return stanza
if isinstance(stanza, (Message, Presence, Iq)):
diff --git a/slixmpp/plugins/xep_0202/time.py b/slixmpp/plugins/xep_0202/time.py
index e7e117a4..2b40dbb2 100644
--- a/slixmpp/plugins/xep_0202/time.py
+++ b/slixmpp/plugins/xep_0202/time.py
@@ -50,7 +50,7 @@ class XEP_0202(BasePlugin):
self.xmpp.register_handler(
Callback('Entity Time',
- StanzaPath('iq/entity_time'),
+ StanzaPath('iq@type=get/entity_time'),
self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)
diff --git a/slixmpp/plugins/xep_0279/ipcheck.py b/slixmpp/plugins/xep_0279/ipcheck.py
index 56d9afd4..8954c3d9 100644
--- a/slixmpp/plugins/xep_0279/ipcheck.py
+++ b/slixmpp/plugins/xep_0279/ipcheck.py
@@ -31,11 +31,11 @@ class XEP_0279(BasePlugin):
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0')
- def check_ip(self, ifrom=None, block=True, timeout=None, callback=None,
+ def check_ip(self, ifrom=None, timeout=None, callback=None,
timeout_callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['from'] = ifrom
iq.enable('ip_check')
- return iq.send(block=block, timeout=timeout, callback=callback,
+ return iq.send(timeout=timeout, callback=callback,
timeout_callback=timeout_callback)
diff --git a/slixmpp/plugins/xep_0313/mam.py b/slixmpp/plugins/xep_0313/mam.py
index 3f543c23..da9ed17b 100644
--- a/slixmpp/plugins/xep_0313/mam.py
+++ b/slixmpp/plugins/xep_0313/mam.py
@@ -3,16 +3,18 @@
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
- See the file LICENSE for copying permissio
+ See the file LICENSE for copying permission
"""
import logging
-import slixmpp
+from datetime import datetime
+from typing import Any, Dict, Callable, Optional, Awaitable
+
+from slixmpp import JID
from slixmpp.stanza import Message, Iq
-from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Collector
-from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0313 import stanza
@@ -41,8 +43,32 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set)
- def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
- timeout=None, callback=None, iterator=False, rsm=None):
+ def retrieve(
+ self,
+ jid: Optional[JID] = None,
+ start: Optional[datetime] = None,
+ end: Optional[datetime] = None,
+ with_jid: Optional[JID] = None,
+ ifrom: Optional[JID] = None,
+ reverse: bool = False,
+ timeout: int = None,
+ callback: Callable[[Iq], None] = None,
+ iterator: bool = False,
+ rsm: Optional[Dict[str, Any]] = None
+ ) -> Awaitable:
+ """
+ Send a MAM query and retrieve the results.
+
+ :param JID jid: Entity holding the MAM records
+ :param datetime start,end: MAM query temporal boundaries
+ :param JID with_jid: Filter results on this JID
+ :param JID ifrom: To change the from address of the query
+ :param bool reverse: Get the results in reverse order
+ :param int timeout: IQ timeout
+ :param func callback: Custom callback for handling results
+ :param bool iterator: Use RSM and iterate over a paginated query
+ :param dict rsm: RSM custom options
+ """
iq = self.xmpp.Iq()
query_id = iq['id']
@@ -53,35 +79,48 @@ class XEP_0313(BasePlugin):
iq['mam']['start'] = start
iq['mam']['end'] = end
iq['mam']['with'] = with_jid
+ amount = 10
if rsm:
for key, value in rsm.items():
iq['mam']['rsm'][key] = str(value)
-
+ if key == 'max':
+ amount = value
cb_data = {}
- def pre_cb(query):
+
+ stanza_mask = self.xmpp.Message()
+ stanza_mask.xml.remove(stanza_mask.xml.find('{urn:xmpp:sid:0}origin-id'))
+ del stanza_mask['id']
+ del stanza_mask['lang']
+ stanza_mask['from'] = jid
+ stanza_mask['mam_result']['queryid'] = query_id
+ xml_mask = str(stanza_mask)
+
+ def pre_cb(query: Iq) -> None:
+ stanza_mask['mam_result']['queryid'] = query['id']
+ xml_mask = str(stanza_mask)
query['mam']['queryid'] = query['id']
collector = Collector(
'MAM_Results_%s' % query_id,
- StanzaPath('message/mam_result@queryid=%s' % query['id']))
+ MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector)
cb_data['collector'] = collector
- def post_cb(result):
+ def post_cb(result: Iq) -> None:
results = cb_data['collector'].stop()
if result['type'] == 'result':
result['mam']['results'] = results
if iterator:
- return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results',
- recv_interface='mam_fin',
+ return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', amount=amount,
+ reverse=reverse, recv_interface='mam_fin',
pre_cb=pre_cb, post_cb=post_cb)
collector = Collector(
'MAM_Results_%s' % query_id,
- StanzaPath('message/mam_result@queryid=%s' % query_id))
+ MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector)
- def wrapped_cb(iq):
+ def wrapped_cb(iq: Iq) -> None:
results = collector.stop()
if iq['type'] == 'result':
iq['mam']['results'] = results
@@ -90,8 +129,15 @@ class XEP_0313(BasePlugin):
return iq.send(timeout=timeout, callback=wrapped_cb)
+ def get_preferences(self, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ query_id = iq['id']
+ iq['mam_prefs']['query_id'] = query_id
+ return iq.send(timeout=timeout, callback=callback)
+
def set_preferences(self, jid=None, default=None, always=None, never=None,
- ifrom=None, block=True, timeout=None, callback=None):
+ ifrom=None, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
@@ -99,7 +145,7 @@ class XEP_0313(BasePlugin):
iq['mam_prefs']['default'] = default
iq['mam_prefs']['always'] = always
iq['mam_prefs']['never'] = never
- return iq.send(block=block, timeout=timeout, callback=callback)
+ return iq.send(timeout=timeout, callback=callback)
def get_configuration_commands(self, jid, **kwargs):
return self.xmpp['xep_0030'].get_items(
diff --git a/slixmpp/plugins/xep_0323/stanza/sensordata.py b/slixmpp/plugins/xep_0323/stanza/sensordata.py
index c0906cac..7ab1e3ba 100644
--- a/slixmpp/plugins/xep_0323/stanza/sensordata.py
+++ b/slixmpp/plugins/xep_0323/stanza/sensordata.py
@@ -516,7 +516,7 @@ class Field(ElementBase):
:param value: string
"""
- pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
+ pattern = re.compile(r"^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
if pattern.match(value) is not None:
self.xml.stringIds = value
else:
diff --git a/slixmpp/plugins/xep_0332/http.py b/slixmpp/plugins/xep_0332/http.py
index bebb0e69..50f14322 100644
--- a/slixmpp/plugins/xep_0332/http.py
+++ b/slixmpp/plugins/xep_0332/http.py
@@ -114,7 +114,6 @@ class XEP_0332(BasePlugin):
iq['http-req']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
- block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
@@ -135,7 +134,6 @@ class XEP_0332(BasePlugin):
iq['http-resp']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
- block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
@@ -153,7 +151,6 @@ class XEP_0332(BasePlugin):
iq['id'] = kwargs["id"]
return iq.send(
timeout=kwargs.get('timeout', None),
- block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
diff --git a/slixmpp/plugins/xep_0363/http_upload.py b/slixmpp/plugins/xep_0363/http_upload.py
index 266fc656..04b066cd 100644
--- a/slixmpp/plugins/xep_0363/http_upload.py
+++ b/slixmpp/plugins/xep_0363/http_upload.py
@@ -28,7 +28,9 @@ class UploadServiceNotFound(FileUploadError):
pass
class FileTooBig(FileUploadError):
- pass
+ def __str__(self):
+ return 'File size too large: {} (max: {} bytes)' \
+ .format(self.args[0], self.args[1])
class HTTPError(FileUploadError):
def __str__(self):
@@ -116,7 +118,7 @@ class XEP_0363(BasePlugin):
except (TypeError, ValueError):
log.error('Invalid max size received from HTTP File Upload service')
self.max_file_size = float('+inf')
- break
+ break
if input_file is None:
input_file = open(filename, 'rb')
@@ -126,7 +128,7 @@ class XEP_0363(BasePlugin):
input_file.seek(0)
if size > self.max_file_size:
- raise FileTooBig()
+ raise FileTooBig(size, self.max_file_size)
if content_type is None:
content_type = guess_type(filename)[0]
@@ -136,6 +138,7 @@ class XEP_0363(BasePlugin):
basename = os.path.basename(filename)
slot_iq = await self.request_slot(self.upload_service, basename, size,
content_type, ifrom, timeout,
+ callback=callback,
timeout_callback=timeout_callback)
slot = slot_iq['http_upload_slot']
diff --git a/slixmpp/plugins/xep_0377/__init__.py b/slixmpp/plugins/xep_0377/__init__.py
new file mode 100644
index 00000000..6ae7a097
--- /dev/null
+++ b/slixmpp/plugins/xep_0377/__init__.py
@@ -0,0 +1,15 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+
+from slixmpp.plugins.xep_0377.stanza import Report, Text
+from slixmpp.plugins.xep_0377.spam_reporting import XEP_0377
+
+
+register_plugin(XEP_0377)
diff --git a/slixmpp/plugins/xep_0377/spam_reporting.py b/slixmpp/plugins/xep_0377/spam_reporting.py
new file mode 100644
index 00000000..e1ca0143
--- /dev/null
+++ b/slixmpp/plugins/xep_0377/spam_reporting.py
@@ -0,0 +1,41 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import slixmpp
+from slixmpp import Message
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+from slixmpp.plugins.xep_0377 import stanza
+from slixmpp.plugins.xep_0191 import Block
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0377(BasePlugin):
+ """XEP-0377: Spam reporting"""
+
+ name = 'xep_0377'
+ description = 'XEP-0377: Spam Reporting'
+ dependencies = {'xep_0030', 'xep_0191'}
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Block, stanza.Report)
+ register_stanza_plugin(stanza.Report, stanza.Text)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=stanza.Report.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(stanza.Report.namespace)
+
diff --git a/slixmpp/plugins/xep_0377/stanza.py b/slixmpp/plugins/xep_0377/stanza.py
new file mode 100644
index 00000000..1930c382
--- /dev/null
+++ b/slixmpp/plugins/xep_0377/stanza.py
@@ -0,0 +1,70 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ET, ElementBase
+
+
+class Report(ElementBase):
+ """
+ A spam/abuse report.
+
+ Example sub stanza:
+
+ <report xmlns="urn:xmpp:reporting:0">
+ <text xml:lang="en">
+ Never came trouble to my house like this.
+ </text>
+ <spam/>
+ </report>
+
+ Stanza Interface:
+ abuse -- Flag the report as abuse
+ spam -- Flag the report as spam
+ text -- Add a reason to the report
+
+ Only one <spam/> or <abuse/> element can be present at once.
+ """
+ name = "report"
+ namespace = "urn:xmpp:reporting:0"
+ plugin_attrib = "report"
+ interfaces = ("spam", "abuse", "text")
+ sub_interfaces = {'text'}
+
+ def _purge_spam(self):
+ spam = self.xml.findall('{%s}spam' % self.namespace)
+ for element in spam:
+ self.xml.remove(element)
+
+ def _purge_abuse(self):
+ abuse = self.xml.findall('{%s}abuse' % self.namespace)
+ for element in abuse:
+ self.xml.remove(element)
+
+ def get_spam(self):
+ return self.xml.find('{%s}spam' % self.namespace) is not None
+
+ def set_spam(self, value):
+ self._purge_spam()
+ if bool(value):
+ self._purge_abuse()
+ self.xml.append(ET.Element('{%s}spam' % self.namespace))
+
+ def get_abuse(self):
+ return self.xml.find('{%s}abuse' % self.namespace) is not None
+
+ def set_abuse(self, value):
+ self._purge_abuse()
+ if bool(value):
+ self._purge_spam()
+ self.xml.append(ET.Element('{%s}abuse' % self.namespace))
+
+
+class Text(ElementBase):
+ name = "text"
+ plugin_attrib = "text"
+ namespace = "urn:xmpp:reporting:0"
diff --git a/slixmpp/plugins/xep_0421/__init__.py b/slixmpp/plugins/xep_0421/__init__.py
new file mode 100644
index 00000000..4595ffad
--- /dev/null
+++ b/slixmpp/plugins/xep_0421/__init__.py
@@ -0,0 +1,13 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.plugins.base import register_plugin
+from slixmpp.plugins.xep_0421.stanza import OccupantId
+from slixmpp.plugins.xep_0421.occupant_id import XEP_0421
+
+register_plugin(XEP_0421)
diff --git a/slixmpp/plugins/xep_0421/occupant_id.py b/slixmpp/plugins/xep_0421/occupant_id.py
new file mode 100644
index 00000000..4ee27a09
--- /dev/null
+++ b/slixmpp/plugins/xep_0421/occupant_id.py
@@ -0,0 +1,32 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp import JID, Message, Presence
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.plugins.xep_0421 import stanza
+from slixmpp.plugins.xep_0421.stanza import OccupantId
+
+
+class XEP_0421(BasePlugin):
+ '''XEP-0421: Anonymous unique occupant identifiers for MUCs'''
+
+ name = 'xep_0421'
+ description = 'Anonymous unique occupant identifiers for MUCs'
+ dependencies = {'xep_0030', 'xep_0045'}
+ stanza = stanza
+ namespace = stanza.NS
+
+ def plugin_init(self) -> None:
+ # XXX: This should be MucMessage. Someday..
+ register_stanza_plugin(Message, OccupantId)
+ register_stanza_plugin(Presence, OccupantId)
+
+ async def has_feature(self, jid: JID) -> bool:
+ info = await self.xmpp['xep_0030'].get_info(jid)
+ return self.namespace in info.get_features()
diff --git a/slixmpp/plugins/xep_0421/stanza.py b/slixmpp/plugins/xep_0421/stanza.py
new file mode 100644
index 00000000..ab1128d6
--- /dev/null
+++ b/slixmpp/plugins/xep_0421/stanza.py
@@ -0,0 +1,41 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 "Maxime “pep” Buquet <pep@bouah.net>"
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.xmlstream import ElementBase
+
+
+NS = 'urn:xmpp:occupant-id:0'
+
+
+class OccupantId(ElementBase):
+ '''
+ An Occupant-id tag.
+
+ An <occupant-id/> tag is set by the MUC.
+
+ This is useful in semi-anon MUCs (and MUC-PMs) as a stable identifier to
+ prevent the usual races with nicknames.
+
+ Without occupant-id, getting the following messages from MUC history would
+ prevent a client from asserting senders are the same entity:
+
+ <message type='groupchat' from='foo@muc/nick1' id='message1'>
+ <body>Some message</body>
+ <occupant-id xmlns='urn:xmpp:occupant-id:0' id='unique-opaque-id1'/>
+ </message>
+ <message type='groupchat' from='foo@muc/nick2' id='message2'>
+ <body>Some correction</body>
+ <occupant-id xmlns='urn:xmpp:occupant-id:0' id='unique-opaque-id1'/>
+ <replace xmlns='urn:xmpp:message-correct:0' id='message1'/>
+ </message>
+ '''
+
+ name = 'occupant-id'
+ plugin_attrib = 'occupant-id'
+ namespace = NS
+ interface = {'id'}
diff --git a/slixmpp/plugins/xep_0444/__init__.py b/slixmpp/plugins/xep_0444/__init__.py
new file mode 100644
index 00000000..dff4287c
--- /dev/null
+++ b/slixmpp/plugins/xep_0444/__init__.py
@@ -0,0 +1,11 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+from slixmpp.plugins.base import register_plugin
+from slixmpp.plugins.xep_0444.reactions import XEP_0444
+
+register_plugin(XEP_0444)
diff --git a/slixmpp/plugins/xep_0444/reactions.py b/slixmpp/plugins/xep_0444/reactions.py
new file mode 100644
index 00000000..bfd12499
--- /dev/null
+++ b/slixmpp/plugins/xep_0444/reactions.py
@@ -0,0 +1,63 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+from typing import Iterable
+
+from slixmpp import JID
+from slixmpp.plugins import BasePlugin
+from slixmpp.stanza import Message
+from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream.matcher import MatchXMLMask
+from slixmpp.xmlstream.handler import Callback
+
+from slixmpp.plugins.xep_0444 import stanza
+
+
+class XEP_0444(BasePlugin):
+ name = 'xep_0444'
+ description = 'XEP-0444: Message Reactions'
+ dependencies = {'xep_0030', 'xep_0334'}
+ stanza = stanza
+ namespace = stanza.NS
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback(
+ 'Reaction received',
+ MatchXMLMask('<message><reactions xmlns="urn:xmpp:reactions:0"/></message>'),
+ self._handle_reactions,
+ )
+ )
+ register_stanza_plugin(Message, stanza.Reactions)
+ register_stanza_plugin(stanza.Reactions, stanza.Reaction, iterable=True)
+
+ def session_bind(self, event):
+ self.xmpp['xep_0030'].add_feature(stanza.NS)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Reaction received')
+ self.xmpp['xep_0030'].remove_feature(stanza.NS)
+
+ def _handle_reactions(self, message: Message):
+ self.xmpp.event('reactions', message)
+
+ def send_reactions(self, to: JID, to_id: str, reactions: Iterable[str], *, store=True):
+ """Send reactions related to a message"""
+ msg = self.xmpp.make_message(mto=to)
+ self.set_reactions(msg, to_id, reactions)
+ if store:
+ msg.enable('store')
+ msg.send()
+
+ @staticmethod
+ def set_reactions(message: Message, to_id: str, reactions: Iterable[str]):
+ """Add reactions to a Message object."""
+ message['reactions']['id'] = to_id
+ for reaction in reactions:
+ reaction_stanza = stanza.Reaction()
+ reaction_stanza['value'] = reaction
+ message['reactions'].append(reaction_stanza)
diff --git a/slixmpp/plugins/xep_0444/stanza.py b/slixmpp/plugins/xep_0444/stanza.py
new file mode 100644
index 00000000..338a244e
--- /dev/null
+++ b/slixmpp/plugins/xep_0444/stanza.py
@@ -0,0 +1,60 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from typing import Set, Iterable
+from slixmpp.xmlstream import ElementBase
+try:
+ from emoji import UNICODE_EMOJI
+except ImportError:
+ UNICODE_EMOJI = None
+
+
+NS = 'urn:xmpp:reactions:0'
+
+class Reactions(ElementBase):
+ name = 'reactions'
+ plugin_attrib = 'reactions'
+ namespace = NS
+ interfaces = {'id', 'values'}
+
+ def get_values(self, *, all_chars=False) -> Set[str]:
+ """"Get all reactions as str"""
+ reactions = set()
+ for reaction in self:
+ value = reaction['value']
+ if UNICODE_EMOJI and not all_chars:
+ if value in UNICODE_EMOJI:
+ reactions.add(reaction['value'])
+ else:
+ reactions.add(reaction['value'])
+ return reactions
+
+ def set_values(self, values: Iterable[str], *, all_chars=False):
+ """"Set all reactions as str"""
+ for element in self.xml.findall('reaction'):
+ self.xml.remove(element)
+ for reaction_txt in values:
+ reaction = Reaction()
+ reaction.set_value(reaction_txt, all_chars=all_chars)
+ self.append(reaction)
+
+
+class Reaction(ElementBase):
+ name = 'reaction'
+ namespace = NS
+ interfaces = {'value'}
+
+ def get_value(self) -> str:
+ return self.xml.text
+
+ def set_value(self, value: str, *, all_chars=False):
+ if UNICODE_EMOJI and not all_chars:
+ if not value in UNICODE_EMOJI:
+ raise ValueError("%s is not a valid emoji" % value)
+ self.xml.text = value
+
diff --git a/slixmpp/stanza/message.py b/slixmpp/stanza/message.py
index f5813eaf..716d48e0 100644
--- a/slixmpp/stanza/message.py
+++ b/slixmpp/stanza/message.py
@@ -10,6 +10,9 @@ from slixmpp.stanza.rootstanza import RootStanza
from slixmpp.xmlstream import StanzaBase, ET
+ORIGIN_NAME = '{urn:xmpp:sid:0}origin-id'
+
+
class Message(RootStanza):
"""
@@ -63,6 +66,8 @@ class Message(RootStanza):
if self['id'] == '':
if self.stream is not None and self.stream.use_message_ids:
self['id'] = self.stream.new_id()
+ else:
+ del self['origin_id']
def get_type(self):
"""
@@ -76,6 +81,43 @@ class Message(RootStanza):
"""
return self._get_attr('type', 'normal')
+ def get_id(self):
+ return self._get_attr('id') or ''
+
+ def get_origin_id(self):
+ sub = self.xml.find(ORIGIN_NAME)
+ if sub is not None:
+ return sub.attrib.get('id') or ''
+ return ''
+
+ def _set_ids(self, value) -> None:
+ if value is None or value == '':
+ return None
+
+ self.xml.attrib['id'] = value
+
+ if not self.stream.use_origin_id:
+ return None
+
+ sub = self.xml.find(ORIGIN_NAME)
+ if sub is not None:
+ sub.attrib['id'] = value
+ else:
+ sub = ET.Element(ORIGIN_NAME)
+ sub.attrib['id'] = value
+ self.xml.append(sub)
+
+ def set_id(self, value):
+ return self._set_ids(value)
+
+ def set_origin_id(self, value: str):
+ return self._set_ids(value)
+
+ def del_origin_id(self):
+ sub = self.xml.find(ORIGIN_NAME)
+ if sub is not None:
+ self.xml.remove(sub)
+
def get_parent_thread(self):
"""Return the message thread's parent thread.
@@ -140,6 +182,8 @@ class Message(RootStanza):
new_message['parent_thread'] = self['parent_thread']
del new_message['id']
+ if self.stream is not None and self.stream.use_message_ids:
+ new_message['id'] = self.stream.new_id()
if body is not None:
new_message['body'] = body
diff --git a/slixmpp/stanza/presence.py b/slixmpp/stanza/presence.py
index 614cd331..7e59e1c5 100644
--- a/slixmpp/stanza/presence.py
+++ b/slixmpp/stanza/presence.py
@@ -90,10 +90,10 @@ class Presence(RootStanza):
def get_type(self):
"""
Return the value of the <presence> stanza's type attribute, or
- the value of the <show> element.
+ the value of the <show> element if valid.
"""
out = self._get_attr('type')
- if not out:
+ if not out and self['show'] in self.showtypes:
out = self['show']
if not out or out is None:
out = 'available'
diff --git a/slixmpp/test/slixtest.py b/slixmpp/test/slixtest.py
index 3953d77d..fbeff3c7 100644
--- a/slixmpp/test/slixtest.py
+++ b/slixmpp/test/slixtest.py
@@ -340,11 +340,19 @@ class SlixTest(unittest.TestCase):
self.xmpp.default_lang = None
self.xmpp.peer_default_lang = None
+ def new_id():
+ self.xmpp._id += 1
+ return str(self.xmpp._id)
+
+ self.xmpp._id = 0
+ self.xmpp.new_id = new_id
+
# Must have the stream header ready for xmpp.process() to work.
if not header:
header = self.xmpp.stream_header
self.xmpp.data_received(header)
+ self.wait_for_send_queue()
if skip:
self.xmpp.socket.next_sent()
@@ -592,6 +600,7 @@ class SlixTest(unittest.TestCase):
'id', 'stanzapath', 'xpath', and 'mask'.
Defaults to the value of self.match_method.
"""
+ self.wait_for_send_queue()
sent = self.xmpp.socket.next_sent(timeout)
if data is None and sent is None:
return
@@ -608,6 +617,14 @@ class SlixTest(unittest.TestCase):
defaults=defaults,
use_values=use_values)
+ def wait_for_send_queue(self):
+ loop = asyncio.get_event_loop()
+ future = asyncio.ensure_future(self.xmpp.run_filters(), loop=loop)
+ queue = self.xmpp.waiting_queue
+ print(queue)
+ loop.run_until_complete(queue.join())
+ future.cancel()
+
def stream_close(self):
"""
Disconnect the dummy XMPP client.
diff --git a/slixmpp/thirdparty/mini_dateutil.py b/slixmpp/thirdparty/mini_dateutil.py
index e751a448..882a531f 100644
--- a/slixmpp/thirdparty/mini_dateutil.py
+++ b/slixmpp/thirdparty/mini_dateutil.py
@@ -160,7 +160,7 @@ except:
return _fixed_offset_tzs[offsetmins]
- _iso8601_parser = re.compile("""
+ _iso8601_parser = re.compile(r"""
^
(?P<year> [0-9]{4})?(?P<ymdsep>-?)?
(?P<month>[0-9]{2})?(?P=ymdsep)?
diff --git a/slixmpp/version.py b/slixmpp/version.py
index feb173e2..757b5473 100644
--- a/slixmpp/version.py
+++ b/slixmpp/version.py
@@ -9,5 +9,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
-__version__ = '1.4.2'
-__version_info__ = (1, 4, 2)
+__version__ = '1.5.2'
+__version_info__ = (1, 5, 2)
diff --git a/slixmpp/xmlstream/stanzabase.py b/slixmpp/xmlstream/stanzabase.py
index 1c000b69..7eaf78a5 100644
--- a/slixmpp/xmlstream/stanzabase.py
+++ b/slixmpp/xmlstream/stanzabase.py
@@ -17,7 +17,7 @@ from __future__ import with_statement, unicode_literals
import copy
import logging
import weakref
-from xml.etree import cElementTree as ET
+from xml.etree import ElementTree as ET
from slixmpp.xmlstream import JID
from slixmpp.xmlstream.tostring import tostring
@@ -203,7 +203,7 @@ class ElementBase(object):
"""
The core of Slixmpp's stanza XML manipulation and handling is provided
- by ElementBase. ElementBase wraps XML cElementTree objects and enables
+ by ElementBase. ElementBase wraps XML ElementTree objects and enables
access to the XML contents through dictionary syntax, similar in style
to the Ruby XMPP library Blather's stanza implementation.
@@ -387,7 +387,7 @@ class ElementBase(object):
self._index = 0
#: The underlying XML object for the stanza. It is a standard
- #: :class:`xml.etree.cElementTree` object.
+ #: :class:`xml.etree.ElementTree` object.
self.xml = xml
#: An ordered dictionary of plugin stanzas, mapped by their
@@ -1031,14 +1031,19 @@ class ElementBase(object):
if not lang:
lang = default_lang
+ parent = self.xml
for level, _ in enumerate(path):
# Generate the paths to the target elements and their parent.
element_path = "/".join(path[:len(path) - level])
parent_path = "/".join(path[:len(path) - level - 1])
elements = self.xml.findall(element_path)
- parent = self.xml.find(parent_path)
-
+
+ if parent_path == '':
+ parent_path = None
+ if parent_path is not None:
+ parent = self.xml.find(parent_path)
+
if elements:
if parent is None:
parent = self.xml
@@ -1374,14 +1379,6 @@ class StanzaBase(ElementBase):
#: The default XMPP client namespace
namespace = 'jabber:client'
- #: There is a small set of attributes which apply to all XMPP stanzas:
- #: the stanza type, the to and from JIDs, the stanza ID, and, especially
- #: in the case of an Iq stanza, a payload.
- interfaces = {'type', 'to', 'from', 'id', 'payload'}
-
- #: A basic set of allowed values for the ``'type'`` interface.
- types = {'get', 'set', 'error', None, 'unavailable', 'normal', 'chat'}
-
def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=None, parent=None):
self.stream = stream
@@ -1500,12 +1497,7 @@ class StanzaBase(ElementBase):
self.name)
def send(self):
- """Queue the stanza to be sent on the XML stream.
-
- :param bool now: Indicates if the queue should be skipped and the
- stanza sent immediately. Useful for stream
- initialization. Defaults to ``False``.
- """
+ """Queue the stanza to be sent on the XML stream."""
self.stream.send(self)
def __copy__(self):
diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py
index 17d23ff2..af494903 100644
--- a/slixmpp/xmlstream/xmlstream.py
+++ b/slixmpp/xmlstream/xmlstream.py
@@ -12,6 +12,8 @@
:license: MIT, see LICENSE for more details
"""
+from typing import Optional, Set, Callable
+
import functools
import logging
import socket as Socket
@@ -19,6 +21,8 @@ import ssl
import weakref
import uuid
+from asyncio import iscoroutinefunction, wait
+
import xml.etree.ElementTree as ET
from slixmpp.xmlstream.asyncio import asyncio
@@ -30,6 +34,10 @@ from slixmpp.xmlstream.resolver import resolve, default_resolver
RESPONSE_TIMEOUT = 30
log = logging.getLogger(__name__)
+class ContinueQueue(Exception):
+ """
+ Exception raised in the send queue to "continue" from within an inner loop
+ """
class NotConnectedError(Exception):
"""
@@ -81,6 +89,8 @@ class XMLStream(asyncio.BaseProtocol):
self.force_starttls = None
self.disable_starttls = None
+ self.waiting_queue = asyncio.Queue()
+
# A dict of {name: handle}
self.scheduled_events = {}
@@ -199,11 +209,6 @@ class XMLStream(asyncio.BaseProtocol):
self.__event_handlers = {}
self.__filters = {'in': [], 'out': [], 'out_sync': []}
- self._id = 0
-
- #: We use an ID prefix to ensure that all ID values are unique.
- self._id_prefix = '%s-' % uuid.uuid4()
-
# Current connection attempt (Future)
self._current_connection_attempt = None
@@ -223,6 +228,8 @@ class XMLStream(asyncio.BaseProtocol):
self.add_event_handler('disconnected', self._remove_schedules)
self.add_event_handler('session_start', self._start_keepalive)
+
+ self._run_filters = None
@property
def loop(self):
@@ -241,12 +248,7 @@ class XMLStream(asyncio.BaseProtocol):
ID values. Using this method ensures that all new ID values
are unique in this stream.
"""
- self._id += 1
- return self.get_id()
-
- def get_id(self):
- """Return the current unique stream ID in hexadecimal form."""
- return "%s%X" % (self._id_prefix, self._id)
+ return uuid.uuid4().hex
def connect(self, host='', port=0, use_ssl=False,
force_starttls=True, disable_starttls=False):
@@ -271,8 +273,15 @@ class XMLStream(asyncio.BaseProtocol):
localhost
"""
+ if self._run_filters is None:
+ self._run_filters = asyncio.ensure_future(
+ self.run_filters(),
+ loop=self.loop,
+ )
+
self.disconnect_reason = None
self.cancel_connection_attempt()
+ self.connect_loop_wait = 0
if host and port:
self.address = (host, int(port))
try:
@@ -297,6 +306,10 @@ class XMLStream(asyncio.BaseProtocol):
async def _connect_routine(self):
self.event_when_connected = "connected"
+ if self.connect_loop_wait > 0:
+ self.event('reconnect_delay', self.connect_loop_wait)
+ await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
+
record = await self.pick_dns_answer(self.default_domain)
if record is not None:
host, address, dns_port = record
@@ -313,7 +326,6 @@ class XMLStream(asyncio.BaseProtocol):
else:
ssl_context = None
- await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
if self._current_connection_attempt is None:
return
try:
@@ -372,6 +384,7 @@ class XMLStream(asyncio.BaseProtocol):
"ssl_object",
default=self.transport.get_extra_info("socket")
)
+ self._current_connection_attempt = None
self.init_parser()
self.send_raw(self.stream_header)
self.dns_answers = None
@@ -430,6 +443,9 @@ class XMLStream(asyncio.BaseProtocol):
self.send(error)
self.disconnect()
+ def is_connecting(self):
+ return self._current_connection_attempt is not None
+
def is_connected(self):
return self.transport is not None
@@ -451,6 +467,8 @@ class XMLStream(asyncio.BaseProtocol):
self.parser = None
self.transport = None
self.socket = None
+ if self._run_filters:
+ self._run_filters.cancel()
def cancel_connection_attempt(self):
"""
@@ -462,11 +480,14 @@ class XMLStream(asyncio.BaseProtocol):
if self._current_connection_attempt:
self._current_connection_attempt.cancel()
self._current_connection_attempt = None
+ if self._run_filters:
+ self._run_filters.cancel()
+
- def disconnect(self, wait=2.0, reason=None):
+ def disconnect(self, wait: float = 2.0, reason: Optional[str] = None, ignore_send_queue: bool = False) -> None:
"""Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has
- passed without a response from the serveur, or when the server
+ passed without a response from the server, or when the server
successfully responds with a closure of its own stream, abort() is
called. If wait is 0.0, this will call abort() directly without closing
the stream.
@@ -476,13 +497,38 @@ class XMLStream(asyncio.BaseProtocol):
:param wait: Time to wait for a response from the server.
"""
+ # Compat: docs/getting_started/sendlogout.rst has been promoting
+ # `disconnect(wait=True)` for ages. This doesn't mean anything to the
+ # schedule call below. It would fortunately be converted to `1` later
+ # down the call chain. Praise the implicit casts lord.
+ if wait == True:
+ wait = 2.0
+
+ if self.transport:
+ if self.waiting_queue.empty() or ignore_send_queue:
+ self.disconnect_reason = reason
+ self.cancel_connection_attempt()
+ if wait > 0.0:
+ self.send_raw(self.stream_footer)
+ self.schedule('Disconnect wait', wait,
+ self.abort, repeat=False)
+ else:
+ asyncio.ensure_future(
+ self._consume_send_queue_before_disconnecting(reason, wait),
+ loop=self.loop,
+ )
+ else:
+ self.event("disconnected", reason)
+
+ async def _consume_send_queue_before_disconnecting(self, reason: Optional[str], wait: float):
+ """Wait until the send queue is empty before disconnecting"""
+ await self.waiting_queue.join()
self.disconnect_reason = reason
self.cancel_connection_attempt()
- if self.transport:
- if wait > 0.0:
- self.send_raw(self.stream_footer)
- self.schedule('Disconnect wait', wait,
- self.abort, repeat=False)
+ if wait > 0.0:
+ self.send_raw(self.stream_footer)
+ self.schedule('Disconnect wait', wait,
+ self.abort, repeat=False)
def abort(self):
"""
@@ -495,14 +541,15 @@ class XMLStream(asyncio.BaseProtocol):
self.event("killed")
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
+ self.event("disconnected", self.disconnect_reason)
def reconnect(self, wait=2.0, reason="Reconnecting"):
"""Calls disconnect(), and once we are disconnected (after the timeout, or
when the server acknowledgement is received), call connect()
"""
log.debug("reconnecting...")
- self.disconnect(wait, reason)
self.add_event_handler('disconnected', lambda event: self.connect(), disposable=True)
+ self.disconnect(wait, reason)
def configure_socket(self):
"""Set timeout and other options for self.socket.
@@ -790,7 +837,7 @@ class XMLStream(asyncio.BaseProtocol):
# If the callback is a coroutine, schedule it instead of
# running it directly
- if asyncio.iscoroutinefunction(handler_callback):
+ if iscoroutinefunction(handler_callback):
async def handler_callback_routine(cb):
try:
await cb(data)
@@ -877,7 +924,9 @@ class XMLStream(asyncio.BaseProtocol):
Execute the callback and remove the handler for it.
"""
self._safe_cb_run(name, cb)
- del self.scheduled_events[name]
+ # workaround for specific events which unschedule themselves
+ if name in self.scheduled_events:
+ del self.scheduled_events[name]
def incoming_filter(self, xml):
"""Filter incoming XML objects before they are processed.
@@ -889,11 +938,93 @@ class XMLStream(asyncio.BaseProtocol):
"""
return xml
+ async def _continue_slow_send(
+ self,
+ task: asyncio.Task,
+ already_used: Set[Callable[[ElementBase], Optional[StanzaBase]]]
+ ) -> None:
+ """
+ Used when an item in the send queue has taken too long to process.
+
+ This is away from the send queue and can take as much time as needed.
+ :param asyncio.Task task: the Task wrapping the coroutine
+ :param set already_used: Filters already used on this outgoing stanza
+ """
+ data = await task
+ for filter in self.__filters['out']:
+ if filter in already_used:
+ continue
+ if iscoroutinefunction(filter):
+ data = await task
+ else:
+ data = filter(data)
+ if data is None:
+ return
+
+ if isinstance(data, ElementBase):
+ for filter in self.__filters['out_sync']:
+ data = filter(data)
+ if data is None:
+ return
+ str_data = tostring(data.xml, xmlns=self.default_ns,
+ stream=self, top_level=True)
+ self.send_raw(str_data)
+ else:
+ self.send_raw(data)
+
+
+ async def run_filters(self):
+ """
+ Background loop that processes stanzas to send.
+ """
+ while True:
+ (data, use_filters) = await self.waiting_queue.get()
+ try:
+ if isinstance(data, ElementBase):
+ if use_filters:
+ already_run_filters = set()
+ for filter in self.__filters['out']:
+ already_run_filters.add(filter)
+ if iscoroutinefunction(filter):
+ task = asyncio.create_task(filter(data))
+ completed, pending = await wait(
+ {task},
+ timeout=1,
+ )
+ if pending:
+ asyncio.ensure_future(
+ self._continue_slow_send(
+ task,
+ already_run_filters
+ )
+ )
+ raise Exception("Slow coro, rescheduling")
+ data = task.result()
+ else:
+ data = filter(data)
+ if data is None:
+ raise ContinueQueue('Empty stanza')
+
+ if isinstance(data, ElementBase):
+ if use_filters:
+ for filter in self.__filters['out_sync']:
+ data = filter(data)
+ if data is None:
+ raise ContinueQueue('Empty stanza')
+ str_data = tostring(data.xml, xmlns=self.default_ns,
+ stream=self, top_level=True)
+ self.send_raw(str_data)
+ else:
+ self.send_raw(data)
+ except ContinueQueue as exc:
+ log.debug('Stanza in send queue not sent: %s', exc)
+ except Exception:
+ log.error('Exception raised in send queue:', exc_info=True)
+ self.waiting_queue.task_done()
+
def send(self, data, use_filters=True):
"""A wrapper for :meth:`send_raw()` for sending stanza objects.
- May optionally block until an expected response is received.
-
:param data: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
stanza to send on the stream.
:param bool use_filters: Indicates if outgoing filters should be
@@ -901,24 +1032,7 @@ class XMLStream(asyncio.BaseProtocol):
filters is useful when resending stanzas.
Defaults to ``True``.
"""
- if isinstance(data, ElementBase):
- if use_filters:
- for filter in self.__filters['out']:
- data = filter(data)
- if data is None:
- return
-
- if isinstance(data, ElementBase):
- if use_filters:
- for filter in self.__filters['out_sync']:
- data = filter(data)
- if data is None:
- return
- str_data = tostring(data.xml, xmlns=self.default_ns,
- stream=self, top_level=True)
- self.send_raw(str_data)
- else:
- self.send_raw(data)
+ self.waiting_queue.put_nowait((data, use_filters))
def send_xml(self, data):
"""Send an XML object on the stream
diff --git a/tests/test_stanza_base.py b/tests/test_stanza_base.py
index 35fa5e99..55554aa9 100644
--- a/tests/test_stanza_base.py
+++ b/tests/test_stanza_base.py
@@ -68,13 +68,5 @@ class TestStanzaBase(SlixTest):
self.assertTrue(stanza['payload'] == [],
"Stanza reply did not empty stanza payload.")
- def testError(self):
- """Test marking a stanza as an error."""
- stanza = StanzaBase()
- stanza['type'] = 'get'
- stanza.error()
- self.assertTrue(stanza['type'] == 'error',
- "Stanza type is not 'error' after calling error()")
-
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)
diff --git a/tests/test_stanza_xep_0377.py b/tests/test_stanza_xep_0377.py
new file mode 100644
index 00000000..321a26a8
--- /dev/null
+++ b/tests/test_stanza_xep_0377.py
@@ -0,0 +1,56 @@
+import unittest
+from slixmpp import Iq
+from slixmpp.test import SlixTest
+import slixmpp.plugins.xep_0191 as xep_0191
+import slixmpp.plugins.xep_0377 as xep_0377
+from slixmpp.xmlstream import register_stanza_plugin
+
+
+class TestSpamReporting(SlixTest):
+
+ def setUp(self):
+ register_stanza_plugin(Iq, xep_0191.Block)
+ register_stanza_plugin(
+ xep_0191.Block,
+ xep_0377.Report,
+ )
+ register_stanza_plugin(
+ xep_0377.Report,
+ xep_0377.Text,
+ )
+
+ def testCreateReport(self):
+ report = """
+ <iq type="set">
+ <block xmlns="urn:xmpp:blocking">
+ <report xmlns="urn:xmpp:reporting:0">
+ <spam/>
+ </report>
+ </block>
+ </iq>
+ """
+
+ iq = self.Iq()
+ iq['type'] = 'set'
+ iq['block']['report']['spam'] = True
+
+ self.check(iq, report)
+
+ def testEnforceOnlyOneSubElement(self):
+ report = """
+ <iq type="set">
+ <block xmlns="urn:xmpp:blocking">
+ <report xmlns="urn:xmpp:reporting:0">
+ <abuse/>
+ </report>
+ </block>
+ </iq>
+ """
+
+ iq = self.Iq()
+ iq['type'] = 'set'
+ iq['block']['report']['spam'] = True
+ iq['block']['report']['abuse'] = True
+ self.check(iq, report)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestSpamReporting)
diff --git a/tests/test_stanza_xep_0421.py b/tests/test_stanza_xep_0421.py
new file mode 100644
index 00000000..dbd7a592
--- /dev/null
+++ b/tests/test_stanza_xep_0421.py
@@ -0,0 +1,29 @@
+import unittest
+from slixmpp import JID, Message
+from slixmpp.test import SlixTest
+import slixmpp.plugins.xep_0421 as xep_0421
+from slixmpp.xmlstream import register_stanza_plugin
+
+
+class TestOccupantId(SlixTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, xep_0421.stanza.OccupantId)
+
+ def testReadOccupantId(self):
+ result = """
+ <message type='groupchat' from='foo@muc/nick1'>
+ <body>Some message</body>
+ <occupant-id xmlns='urn:xmpp:occupant-id:0' id='unique-id1'/>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['type'] = 'groupchat'
+ msg['from'] = JID('foo@muc/nick1')
+ msg['body'] = 'Some message'
+ msg['occupant-id']['id'] = 'unique-id1'
+
+ self.check(msg, result)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestOccupantId)
diff --git a/tests/test_stanza_xep_0444.py b/tests/test_stanza_xep_0444.py
new file mode 100644
index 00000000..b4d5739b
--- /dev/null
+++ b/tests/test_stanza_xep_0444.py
@@ -0,0 +1,69 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2020 Mathieu Pasquet
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import unittest
+from slixmpp import Message
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0444 import XEP_0444
+import slixmpp.plugins.xep_0444.stanza as stanza
+from slixmpp.xmlstream import register_stanza_plugin
+
+
+class TestReactions(SlixTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, stanza.Reactions)
+ register_stanza_plugin(stanza.Reactions, stanza.Reaction)
+
+ def testCreateReactions(self):
+ """Testing creating Reactions."""
+
+ xmlstring = """
+ <message>
+ <reactions xmlns="urn:xmpp:reactions:0" id="abcd">
+ <reaction>😃</reaction>
+ <reaction>🤗</reaction>
+ </reactions>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['reactions']['id'] = 'abcd'
+ msg['reactions']['values'] = ['😃', '🤗']
+
+ self.check(msg, xmlstring, use_values=False)
+
+ self.assertEqual({'😃', '🤗'}, msg['reactions']['values'])
+
+
+ def testCreateReactionsUnrestricted(self):
+ """Testing creating Reactions with the extra all_chars arg."""
+
+ xmlstring = """
+ <message>
+ <reactions xmlns="urn:xmpp:reactions:0" id="abcd">
+ <reaction>😃</reaction>
+ <reaction>🤗</reaction>
+ <reaction>toto</reaction>
+ </reactions>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['reactions']['id'] = 'abcd'
+ msg['reactions'].set_values(['😃', '🤗', 'toto'], all_chars=True)
+
+ self.check(msg, xmlstring, use_values=False)
+
+ self.assertEqual({'😃', '🤗'}, msg['reactions']['values'])
+ self.assertEqual({'😃', '🤗', 'toto'}, msg['reactions'].get_values(all_chars=True))
+ with self.assertRaises(ValueError):
+ msg['reactions'].set_values(['😃', '🤗', 'toto'], all_chars=False)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestReactions)
diff --git a/tests/test_stream_xep_0323.py b/tests/test_stream_xep_0323.py
index 7c9cc7e8..baacd7d3 100644
--- a/tests/test_stream_xep_0323.py
+++ b/tests/test_stream_xep_0323.py
@@ -4,6 +4,7 @@ import sys
import datetime
import time
import threading
+import unittest
import re
from slixmpp.test import *
@@ -11,6 +12,7 @@ from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0323.device import Device
+@unittest.skip('')
class TestStreamSensorData(SlixTest):
"""