pax_global_header00006660000000000000000000000064147710556000014517gustar00rootroot0000000000000052 comment=13b120b8f6818d9a0fb72fa6a0d66926b1306002 slixmpp/000077500000000000000000000000001477105560000125575ustar00rootroot00000000000000slixmpp/.gitignore000066400000000000000000000003351477105560000145500ustar00rootroot00000000000000*.py[co] build/ dist/ MANIFEST docs/_build/ *.swp .tox/ .coverage slixmpp.egg-info/ .ropeproject/ 4913 *~ .baboon/ .DS_STORE .idea/ .vscode/ venv/ .venv .python-version slixmpp/*.so # Added by cargo /target /Cargo.lock slixmpp/.readthedocs.yaml000066400000000000000000000010751477105560000160110ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt slixmpp/.woodpecker/000077500000000000000000000000001477105560000147775ustar00rootroot00000000000000slixmpp/.woodpecker/build.yml000066400000000000000000000006121477105560000166200ustar00rootroot00000000000000when: event: [ tag ] steps: build-and-publish: image: ghcr.io/pyo3/maturin environment: TOKEN: from_secret: codeberg_package_nicoco commands: - maturin publish -i $TAG --username nicoco --password $TOKEN --repository-url https://codeberg.org/api/packages/poezio/pypi/ --no-sdist matrix: TAG: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" slixmpp/.woodpecker/container-ci.yaml000066400000000000000000000004231477105560000202350ustar00rootroot00000000000000when: event: [ manual ] steps: build-and-push: image: woodpeckerci/plugin-docker-buildx settings: repo: codeberg.org/poezio/woodpecker-slixmpp registry: codeberg.org username: nicoco password: from_secret: codeberg_package_nicoco slixmpp/.woodpecker/lint.yml000066400000000000000000000004441477105560000164720ustar00rootroot00000000000000when: event: [ push, pull_request ] path: [ "**/*.py" ] steps: setup_venv: image: codeberg.org/poezio/woodpecker-slixmpp pull: true commands: - uv sync --frozen --only-group dev mypy: image: codeberg.org/poezio/woodpecker-slixmpp commands: - mypy slixmpp slixmpp/.woodpecker/test-integration.yml000066400000000000000000000010721477105560000210220ustar00rootroot00000000000000when: event: [ push, pull_request ] path: [ "**/*.py" ] steps: test_integration: image: codeberg.org/poezio/woodpecker-slixmpp pull: true environment: CI_ACCOUNT1: from_secret: ci_account1 CI_ACCOUNT1_PASSWORD: from_secret: ci_account1_password CI_ACCOUNT2: from_secret: ci_account2 CI_ACCOUNT2_PASSWORD: from_secret: ci_account2_password CI_MUC_SERVER: from_secret: ci_muc_server commands: - uv sync --frozen --all-extras --all-groups - ./run_integration_tests.py slixmpp/.woodpecker/test.yml000066400000000000000000000006421477105560000165030ustar00rootroot00000000000000when: event: [ push, pull_request ] path: [ "**/*.py" ] steps: setup_venv: image: codeberg.org/poezio/woodpecker-slixmpp pull: true commands: - uv python pin ${TAG} - uv sync --frozen --all-groups --all-extras unit_tests: image: codeberg.org/poezio/woodpecker-slixmpp commands: - ./run_tests.py matrix: TAG: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" slixmpp/CONTRIBUTING.rst000066400000000000000000000010631477105560000152200ustar00rootroot00000000000000Contributing to the Slixmpp project =================================== To contribute, the preferred way is to commit your changes on some publicly-available git repository (on a fork `on codeberg `_ or on your own repository) and to notify the developers with either: - a merge request `on the repository `_ - a ticket `on the bug tracker `_ - a simple message on `the XMPP MUC `_ slixmpp/Cargo.toml000066400000000000000000000004761477105560000145160ustar00rootroot00000000000000[package] name = "slixmpp" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] jid = "0.12" pyo3 = { version = "0.23", features = ["extension-module"] } [lib] name = "jid" crate-type = ["cdylib"] path = "slixmpp/jid.rs" slixmpp/Containerfile000066400000000000000000000011651477105560000152670ustar00rootroot00000000000000# A container for Woodpecker CI # This it NOT meant to be used in any other context FROM debian:trixie-slim ENV UV_LINK_MODE=copy ENV PATH=.venv/bin:$PATH RUN apt update && apt install cargo gpg -y COPY --from=ghcr.io/astral-sh/uv:0.5.26 /uv /uvx /bin/ # install different python versions and populate the pypi cache, # this way woodpecker does not have to make network calls unless # we change the dependencies COPY pyproject.toml uv.lock . RUN for VER in 3.9 3.10 3.11 3.12 3.13; do \ uv python install $VER ; \ uv python pin $VER ; \ uv sync --frozen --all-groups --no-install-project ; \ done slixmpp/LICENSE000066400000000000000000000157351477105560000135770ustar00rootroot00000000000000Copyright (c) 2010 Nathanael C. Fritz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Licenses of Bundled Third Party Code ------------------------------------ dateutil - Extensions to the standard python 2.3+ datetime module. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Copyright (c) 2003-2011 - Gustavo Niemeyer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. fixed_datetime ~~~~~~~~~~~~~~ Copyright (c) 2008, Red Innovation Ltd., Finland All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Red Innovation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SUELTA – A PURE-PYTHON SASL CLIENT LIBRARY ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This software is subject to "The MIT License" Copyright 2004-2013 David Alan Cridland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-gnupg: A Python wrapper for the GNU Privacy Guard ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Copyright (c) 2008-2012 by Vinay Sajip. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name(s) of the copyright holder(s) may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. slixmpp/MANIFEST.in000066400000000000000000000002701477105560000143140ustar00rootroot00000000000000include README.rst include LICENSE include run_tests.py recursive-include docs Makefile *.bat *.py *.rst *.css *.ttf *.png recursive-include examples *.py recursive-include tests *.py slixmpp/README.rst000066400000000000000000000130011477105560000142410ustar00rootroot00000000000000Slixmpp ######### 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 socket handling, the timers, the events dispatching) in order to remove all threads. Building -------- Slixmpp uses rust to improve performance on critical modules. Binaries may already be available for your platform in the form of `wheels `_ provided on PyPI or packages for your linux distribution. If that is not the case, `cargo `_ must be available in your path to build the `extension module `_. Documentation and Testing ------------------------- Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``. To generate the Sphinx documentation, follow the commands below. The HTML output will be in ``docs/_build/html``:: cd docs make html open _build/html/index.html To run the test suite for Slixmpp:: python run_tests.py Integration tests require the following environment variables to be set::: $CI_ACCOUNT1 $CI_ACCOUNT1_PASSWORD $CI_ACCOUNT2 $CI_ACCOUNT2_PASSWORD $CI_MUC_SERVER where the account variables are JIDs of valid, existing accounts, and the passwords are the account passwords. The MUC server must allow room creation from those JIDs. To run the integration test suite for Slixmpp:: python run_integration_tests.py The Slixmpp Boilerplate ------------------------- Projects using Slixmpp tend to follow a basic pattern for setting up client/component connections and configuration. Here is the gist of the boilerplate needed for a Slixmpp based project. See the documentation or examples directory for more detailed archetypes for Slixmpp projects:: import asyncio import logging from slixmpp import ClientXMPP from slixmpp.exceptions import IqError, IqTimeout class EchoBot(ClientXMPP): def __init__(self, jid, password): ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) # If you wanted more functionality, here's how to register plugins: # self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0199') # XMPP Ping # Here's how to access plugins once you've registered them: # self['xep_0030'].add_feature('echo_demo') # If you are working with an OpenFire server, you will # need to use a different SSL version: # import ssl # self.ssl_version = ssl.PROTOCOL_SSLv3 def session_start(self, event): self.send_presence() self.get_roster() # Most get_*/set_* methods from plugins use Iq stanzas, which # can generate IqError and IqTimeout exceptions # # try: # self.get_roster() # except IqError as err: # logging.error('There was an error getting the roster') # logging.error(err.iq['error']['condition']) # self.disconnect() # except IqTimeout: # logging.error('Server is taking too long to respond') # self.disconnect() def message(self, msg): if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') xmpp = EchoBot('somejid@example.com', 'use_getpass') xmpp.connect() asyncio.get_event_loop().run_forever() Slixmpp Credits --------------- **Maintainers:** - Florent Le Coz (`louiz@louiz.org `_), - Mathieu Pasquet (`mathieui@mathieui.net `_), **Contributors:** - Emmanuel Gil Peyrot (`Link mauve `_) - Sam Whited (`Sam Whited `_) - Dan Sully (`Dan Sully `_) - Gasper Zejn (`Gasper Zejn `_) - Krzysztof Kotlenga (`Krzysztof Kotlenga `_) - Tsukasa Hiiragi (`Tsukasa Hiiragi `_) - Maxime Buquet (`pep `_) Credits (SleekXMPP) ------------------- **Main Author:** Nathan Fritz `fritzy@netflint.net `_, `@fritzy `_ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP `_, and a former member of the XMPP Council. **Co-Author:** Lance Stout `lancestout@gmail.com `_, `@lancestout `_ **Contributors:** - Brian Beggs (`macdiesel `_) - Dann Martens (`dannmartens `_) - Florent Le Coz (`louiz `_) - Kevin Smith (`Kev `_, http://kismith.co.uk) - Remko Tronçon (`remko `_, http://el-tramo.be) - Te-jé Rogers (`te-je `_) - Thom Nichols (`tomstrummer `_) slixmpp/doap.xml000066400000000000000000001323721477105560000142340ustar00rootroot00000000000000 slixmpp 2010-01-10 Elegant Python library for XMPP Bibliothèque pour XMPP élégante, en Python en Python Linux macOS FreeBSD OpenBSD NetBSD Link Mauve aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f louiz’ a867767905969a4915147374e3a064f97cdf5d61 mathieui c14292b375a7cec3f39872aa8524c66a1d9106cf pep. 29ed31759e39e0da3f3634e91b667275ba5e4ac6 complete 2.11.0 1.0 complete 2.2 1.0 complete 2.0 1.0 complete 1.2 1.0 complete 1.7 1.0 The XEP is deprecated, we might remove support for it at some point. complete 1.6 1.0 complete 1.4 1.0 complete 2.5rc3 1.0 complete 1.2.1 1.0 partial 1.34.0 1.0 complete 2.0 1.0 complete 1.2 1.0 The XEP is deprecated, we might remove support for it at some point. complete 1.2 1.0 complete 1.3.0 1.0 complete 1.2 1.0 complete 1.0 1.0 partial 1.19.0 1.0 complete 1.8.1 1.0 complete 1.5 1.0 complete 1.0.1 1.2 complete 1.5.4 1.0 This XEP is deprecated, but we have no plan to remove support for it. complete 2.4 1.0 complete 2.5 1.0 This XEP is obsolete, we are considering removing support for it. complete 1.2 1.0 complete 1.9 1.0 complete 1.1 1.0 complete 1.1.4 1.0 complete 2.1 1.0 complete 1.0 1.0 The XEP is deprecated, we might remove support for it at some point. complete 1.4 1.0 This XEP is obsolete, we are considering removing support for it. complete 1.1 1.0 complete 1.2 1.0 The XEP is deprecated, we might remove support for it at some point. complete 1.3 1.0 The XEP is deprecated, we might remove support for it at some point. complete 1.1.1 1.0 complete 1.2.1 1.0 complete 1.3 1.0 complete 1.5.2 1.0 complete 1.3.0 1.0 complete 1.0.2 1.1 complete 1.0.1 1.0 complete 1.2 1.0 complete 1.2 1.0 complete 1.0 1.0 complete 1.1 1.0 complete 1.2.1 1.0 complete 1.1 1.0 complete 1.2 1.0 complete 1.4.0 1.0 complete 0.14.0 1.0 complete 1.3 1.0 complete 0.3 1.0 complete 1.6 1.0 complete 2.0.1 1.0 complete 2.0 1.0 complete 2.0 1.0 complete 1.0 1.0 complete 1.0 1.0 complete 1.1 1.0 complete 1.0 1.0 complete 1.0 1.0 complete 0.7 1.0 complete 1.0 1.0 This XEP is obsolete, we are considering removing support for it. complete 1.2 1.0 complete 1.1 1.2 complete 0.3 1.0 complete 1.1.1 1.0 complete 0.4.2 1.8.6 complete 1.1 1.0 This XEP is obsolete, we are considering removing support for it. complete 0.2 1.0 complete 0.13.2 1.0 complete 1.0 1.0 complete 1.0.0 1.3.0 complete 0.1 1.0 This XEP is obsolete, we are considering removing support for it. complete 1.2.0 1.0 complete 0.7.2 1.0 complete 1.8.6 0.2 complete 1.0.2 1.0 complete 0.6 1.0 This XEP has been retracted, we are considering removing support for it. complete 0.5 1.0 This XEP has been retracted, we are considering removing support for it. complete 0.5.1 1.1 complete 0.4 1.2 complete 0.3.0 1.2 complete 0.1.1 1.5.0 complete 1.0.0 1.2 complete 0.3 1.6.0 complete 0.6.1 1.6.0 complete 1.0.0 1.4.1 complete 1.1.0 1.10.0 partial 0.14.6 1.6.0 complete 0.3 1.6.0 complete 0.3.0 1.3.0 complete 0.2.0 1.7.0 complete 0.2.1 1.3.0 partial 0.3.2 1.6.0 partial 0.3.1 1.6.0 partial 0.5.1 1.6.0 complete 1.1.0 1.9.0 complete 0.1.0 1.6.0 complete 0.2.0 1.6.0 complete 0.4.0 1.6.0 complete 0.3.0 1.6.0 complete 0.1.1 1.6.0 complete 0.2.0 1.6.0 complete 0.1.0 1.6.0 complete 0.1.0 1.6.0 complete 0.2.0 1.9.0 partial 0.1.0 1.8.1 no thumbnail support partial 0.1.0 1.8.6 complete 0.1.0 1.9.0 complete 0.1.0 1.8.6 complete 0.1.0 1.9.0 1.0 2015-07-31 1.1 2015-10-02 1.2 2016-10-02 1.2.1 2016-10-05 1.2.2 2016-11-21 1.2.3 2016-12-07 1.2.4 2017-01-30 1.3.0 2017-11-28 1.4.0 2018-08-12 1.4.1 2018-10-28 1.4.2 2019-01-31 1.5.0 2020-05-01 1.5.1 2020-05-02 1.5.2 2020-05-23 1.6.0 2020-12-12 1.7.0 2021-01-29 1.7.1 2021-04-30 1.8.0 2022-02-27 1.8.1 2022-03-20 1.8.2 2022-04-06 1.8.3 2022-11-12 1.8.4 2023-05-28 1.8.5 2024-02-02 1.8.6 2024-12-26 1.9.0 2025-02-27 1.9.1 2025-03-11 1.10.0 2025-03-26 slixmpp/docs/000077500000000000000000000000001477105560000135075ustar00rootroot00000000000000slixmpp/docs/.gitignore000066400000000000000000000000111477105560000154670ustar00rootroot00000000000000_build/* slixmpp/docs/Makefile000066400000000000000000000107621477105560000151550ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Slixmpp.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Slixmpp.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Slixmpp" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Slixmpp" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." slixmpp/docs/_static/000077500000000000000000000000001477105560000151355ustar00rootroot00000000000000slixmpp/docs/_static/images/000077500000000000000000000000001477105560000164025ustar00rootroot00000000000000slixmpp/docs/_static/images/arch_layers.png000066400000000000000000000657751477105560000214300ustar00rootroot00000000000000PNG  IHDR Jv pHYs   IDATx |ǟA: qq-uTQTUuEJݥn& "3ɻ6Molb|>}gwn3K                           C@-l"¥&izMq|nyN-[OO%ö )" 7^C]E Ek]mS+C7;L#`UPi:fbTi]\XT]>ʜR{;t]r]>CC XOaNjmAA@싀UBFu-5%5׊Ei=싈VA'/?vu֠=J   ` - 5M_MѮʝ:N5 ֨ ʈi9t^7?w.JMVs9Eo\FkA@ 6<-L?7LjQ%xA&Jw*M}%tb=4ٶB~x_y=n~ O3W)*t!C@FOBO,ZD 6X4݂~Z6Et;۠XرyTj  `%bJ%j1;8jsuB(ܺW-}K4WbQ3H,Dz1oѵ`?RBq]#"OQS[!`Xx:}Qa&~}()(},\ٜrBQXɠ'} I {Ύ^0&-9-2@/vRfgQςFC=өѫ`3NMEd pSȝ K2wm  0z*`#ggAl 7<= s}{%IP:(xJ{Wi)Ʀq[8ObAR LaNCbi.5LʔL]zMտ'ݼv-?J-(8Z-GN;/AV9 :92  "@1(ςXD$f+=?}^/P| E8Jaa Td.?#}Ѹ2{2W/QMrT˓& C_t4wXjSF]Ge:i v@^ ^  3IGO9<ONuYFYޫ,kÕxjYn2ʅ?oDϢ^'0kZ6ooUCv@gޮ *̓J(sx9sǛ[y6*Oq.?Cc|'*׬yi*+̖B,'~meY?:v!uۻs~ê%rΊ#WiHK:1d,BUA@-Vу{(Utx}jsiϢ\H>o\i1-W9޳s>'Ԡf:ź8f*Λ_ѥ'-3?5اJ.:Ys jД|C~ F;%;p6G ?TSД#y;qfUfn̈́0->dN?mC7lsXtlrOv[L%PBh*WZ .fH$8D" q}n)8>fWQ 7 6{%{r7A,CFTFPҙGhh﮴p92",NbQa>F :l=D(YH ӠY+a}nM-#SI|L=h4}E'C~쀀@ς]1, 0Dp$ϘFLC<qpnL6=yT`,M΋J)f- ¹ǣ%;},MA|֞VnO_c/>@^ >u9e.e9#x"&IRty6^J__vpPz)A|M/PPѧ,RY@ I@,$ fߨܑ/;|ٓ5U LʣYC;3|ЕxEnTv1yٚL!ŒG>3~)R0V:-%f;gK$88eA@ aZ$(<`:7=rS[(0R|e[Y)Sl29Lوgkoy!T A@բو_‚g.!^!{c8NvGos#WbV6ĂZbK?mK|@JCk-@nٸ;:cKίhkz˺!L i!an0b!^D0pFO< жЭ(, e͖C9ԧI9B;# V8vlQ6Z}0u h]\]oǺ%4p| -"‡  @Uh={M m8=} 'b-U'1Ă %-X1   `JVЋi@S㸎CcHWl\H(B,|@85K =p4jcA@@!@,8eD#@@@ `"آd$!f K[ᵘ>!U(|%P;}ky!l ~  < ~NoĴ@/B^Sȫr10Gn|C:܍{22qO>OM)Gb j ͘osEތ?{AaoފPq~i#onܲb 1[A4%2 ] %?.9  6D@?yB<Ϳ%+O$RD|P>OTn|󌼁9Pp= 7r'5)\/~qV !?1Q[ǴrNd@ >N  XuOe:+O o, !j wsӱ?%}NOb_p-i;绛vgvg=g&Wm?͝_n~s~H+i/^9.oJ7:/#n.^G"^G&׈x}D8|H"+DQ OOA_t 鋗l_7'PN-HJL⒋T)RPc=/"=ܽ: =x"#丿2ŏWp tGvOz mcK"j GbYט=ŏqw`~VKR !DhqDE, 'cqs$sJȒA+ yS=[YWYAOKeFcbn_ ֒_>zNoEc €qͱ @  袛"2Mn& sk\\5zMN6 f]nIO|8Nfnv2ND='͆7$O&z<<"{<8{JrOҐO'4S {7B&z#Z8c?,mQyr77rev_y#mc$VR(HX1O7yrB;Vbb@X`m/N J )+v)ƍ]u.ZWBrCѸ Q" iH/G}dD&qqx?RR~E@>>wVCO7{޾{p5| | 'Ƨ[wCtk3Б.V{o,Zbxƚ"AOA,X,ci5q&e%.!QbO@L  ! nߨ :b| @@Zf-(@@Ă^X4 @@EbZ$Q8(,8MfԬ.B_4z/?_DY yN z hg\9ֺh  Ob:E =Z]{tZ#&!@BCi({ꙡ&&o `5 VC4]H1Q>{>Ȕ9URG)mԲώIEaaat#q͟ΜB.+iiڐ֥ u&[;HbZչx)Ky6>2ed$N U\wjFŲQ7mڈ&B4tu܈PCjh9gƦEDI3#qhʌ]}쥩գWZ| UY;x߄`i/m@=A @,@u bx8}sCrUˈ6NEQ=wQV'C16^mTlĂ_ =VQ5[4}@飐@4nV&xȰ<vyĂs_xDp`߱U16Lz o?+YC1{b&puQ5i 6}yr=B@z'><7XaA"ESXp#y^GSٲs=G3W_J+ * `s l| ʐU[EǍ}J MFdѻ/N"A @,8%Ė>o"hԴhޅʏ *@,娦rBKkz;j3.'&$VCqѺPtT޻2}*!Y!#v@z:WXt-+w`7=yLoB4aۨ=B⳺0r$ʞ='IE=؉CPȕ3 <ސC:p/y o3{Tp&<=Dz4k+DXq^/v wP:td/e˚]>=p.K_ϟw?t ":a|mrٹk!YM..4uϔ:{9s qQ16}ڵ*O/B|!4LZ{8   3d. 3m\ԫG?lweݸy/{K^ֹJBg9{RTT3Z|\ZK#D9r@\> `7 R~c[ʚ#dyܡ]W ԡt7g3RS Ta99{"E+C˾nE^':91=9Eh},ի֊3Z8 C"IT)-E?}S[Ky!;r̤ͪOxXȓ;b'E(R=yD Ρ+拧x~L'Ǹ ,!<7zeʘYJcFN}M,&!Ga/eJ<&4}jzKҐ(S|{) 1o}r.ry/!mq/r^ŨTzMid@._{w_|yh>#Yr= uUϛ=m?j׬Q{Jp0⟠,뜐@s/pzVB( =)íHi$ ֥햠W PC!OWIV/&^*p0RjYU>]`|@*Xxqt]_0j@vOo<j3aeREŋxEEUvط@峅,4ERN۲ƻF%(wJo%>wҕhǚK84Cz["\Ym։`_.^!SĔ8=%i=34]":MџJy{렩A tl3*.y7G<} @> xR )C挦&8#u(-8N`Ԅc5 ZpX=aa {a)RQ}w)xM6nZ  j@! {qZtiK2D E/bnE{|umxWAb!8HpMxgVXڑ p9GJg"%g6*XӸMVGM!Ђ-cp #c^a*@‘ӷuZM5P Ђ-C)șa]D4 $7{AcB@ςU0p\ {m2 `D!    ڢe   ` VB@@@q ER{m2"}.uz:s|I&u-ZQZ46TSʅdb|3mؾ x=kOiH{)ݺs>o)4f2SY^.yT`w\vص\Wn\#] eN1/pѺGtTٻ =_АG!cg!t EA=iI ~iqTq-^炒TfJ4t(FL-mȤR(ə& |UǏCs'Z)~ =ЃG̙ĈEP'Ըg1"eM]DCA@Ǡh=k6ze wغz}Ս=N{ع k뗭_mew՛֓ ͛#N-o9i7m6<?B9}dmvṛ|^[Py<Xs<-o-Q@"G@,$2` v'{2eH;Vng7)\iP߄&4yA%b+)Gb!gz%T,ܸ,*KLJqnY{&YD'<cK~B/_GЖ GĞn\8GmN\~ݿ}G#nttns"uOoLC%],ϳuyK}BómK.=d9<D s/BlҴwqTt!)X[W+AW-+UFV(M~M{xPE!3{(08>GzX¨Ϩ!ҾVqSkgaVJXH ≖N'- MuyhY=(iҐQTPJ"ukI´(R4|+B8< ~*~گ]y*xyӔacdrԇg)ۭ]'Y^w]TɓӢɠp9sW3/"8BX JƟЎM^V0g!x%PtYy#n8 9y 5F"l^Z>+:ly4h5Zb_qP,b\P̥q/!XjG7t Vl ~\ђ%Z褺J?o&R挑xQi#I;EL2(C3Γ>*W0*w1 a e &ϒkb@_M59 ܍ 'F0Bݍ{?sL԰}h-QKҝvM'P6/Jʡ[9ޛ%J=0~ j`"i%WrfHڻfKsŗZ,d8])Fv;x֮!.偀]N}߄fjÂ瀻ydSp_Ѷke59UE\] 1•ϓs[Mncz=X\LOT N<ݔ {&{bر]7lq++3'/jeR<{o_Rej{e!of?E5?2DzmKP-{fhDu73kL1"6OQ-:6! +ÜyPi:9glKwk_v,8Ŵ 䨧ALSp C )DB4 D$phUkʨ)$p7!&[Oe0g%W>Z҅bqDw$Î.BRPsm'MB0BLz .ā2*8PДD$p\ߝeg յ?7Еe0y pf |ڮiv{w#%ce9KucPy >t[<&Q:ڜbQ'H'/A1B.5LPIKby}xKrIוb# 2]N<_K @,XJ "pIJ?'h5x]Q(k+ڪqq"QLA Nk_A,O / nQia@{#3{F(.n"( 8(!ݏ1 `ކKGԴbA -'d4a5iBZh/WqMK _:j{6g/8gT-Nfb!@(1SJMTr䝿2}Z3=gzppO į+Rߵ ? GOPCcmG1%;?=tv5̞HZxHWN_ WDLj3$W'_Qȋwo޼K5oVP欙g޴g>BRW!wɖEC:Sh|TaUR͋gHF~]'eY 5+Q(|6<.]C&Kh4aX)}oS< dmfM_AYe$|9(]4qT20{ݾuW CzڤG7l Iz?`{SE)GY  t"yF3v ͋'#=͟$d6&siԨ'y,$(!&韹$x|yj|X?ћUѯgW߷.[jqV?!""UKn$N@n /Qq[ݟ6ؼXȔ9#ݿH<6 3 9RR&e_s.Zd ]>^ E Yk\~қRtoˁiIwLIҀ5*2hs2e(_|?cb=~ZrW9N'5bXP,-rj:sqWj q@y ZMH8E|B>ixQԾWYS&.[%OfQgNMsϏf{/>-!_cA)i_&@;yX "rʲx9DOr> GY +l~6gZt҂Sf_ZvK;;rPz}xi'9ENӒ d[?y xҮc[}pƟgQT)]B6!5A@@|Cjca#ɣԱtqZh5-۰2e1Kxq+peڲ~t!|   `kB,(Jx'~h?cb' ~ebm#޾X0 b4J*R9=tQ"2hڰzK?i5 9"`b˳~CYM.b#"3x y:עE RŨiOcc!bر?CN=l;fHj'rh@@lMߧekAb 'r&V#M8%$~۾2dzk3p*qќ Pn to-2f@sͤ2Lq$H]-I&y ST?IE T.j5tUtn{~fE+ZZavpd?\b0_;R.}dzuV+"#Cbq i?7[yvR(pO] .絤naֻzcr5j.gP >B(x-BbG  .Fj:m rwCzPhܥbcڌSs 5܄NIu֢±qC4%.*5*Fz;Tlv: >o%rD>ٴ%«!/%c]zsQzˑ"eW(@O"޺E:CO5C{ CR|Cc%lZ,px1*>iXTܙ q&#ذY}Խaq̆p<]ěuTx!m'u;m jiIP絤nٸ)Elk{R_lAԯq8 GYNJ-߰6Eo޾RWB_ާ˓Z{fJ*,H i|6DA/ѐ)EOm WӖ) N  }(cԌ! @@{'LL鯦|Zѥʨ60}up~7G h V"@@Cȗ;'͟0x+<銼J||4!uXHAOO]Orq}60"=Óo詉{QŻ rlC~-al /rVu  eD)A+z޽AC,X"4CE]֩%dh8JDȖڶC,%b KmMk۴#m'СE3@J- >ˈYSo@@wji}̿}1f۴Xػs};Z܇(CUդo:{mN,%?K+'b'{ Z׵|Re@@@%^d"2"_ZMMPs%y?슖Wgd&6 PɣAO!h:^{xoM66ʔ1c0_3D#tOiqI0 iҺS>=J(kS:o5im};HcqcľP ^@!|(cz{6   @DNXaw9HKkwJ{~)Bd5$7$>c#!]ؒ  \޽YUg͉~^)_f/ N=:ʴu+# _uDeV#/Rl,pc~:Rew9qJwi`TH Ģ%mYMI3a|NG3'͖/Dm n4"U")p椟?{KZ? G쮉R0uI릆xӝicf9K oS票cǯҰ  Dz(̕#NN=s{bLeŀ3۶#WvWJ/yy [XKO7l6b (FO0dQmy͆alX<(i&.-: ֆO/ߐŚ[Cy   lr":Zo#/^XbCp5+*R  8X`HujBX0|@@@$Tq@"{zmq!RŊQk'}G;D ) x%}޸!OM[1Ӧ˴R%z+ũAfb}/~2ں^H7]?0kŅҥ RB < Xz Ł#7'Oҷ:ҩcG%|cG9r1b)rMFK{޷I|fEˣ̛#-x>zVAOcӗ-jL>|!q…gM[( )4mQb $`M( @@WI͘)}?hxϐ!wʔ//KVw@y9.)UUV?{FFK;u^ ժE@R.OO޻K/iԔiY,?p[:@3|^@<ǥshOJqb8Mtab!3 t[JUYƑ۾ eWjyς?7n̙x o0x  ` W>жӧmJU!t?423*|%/}* -V/["+"BI2fN:}mصJovr##ʕoic?]bfGqr}91~,lF(M<$ qtY`S4>wۦSO}D<@~N,~](nCȁ#:Y2^y:svQF[(fW+/PO[ґ! /yXtCaZFB򘖁c,X"00?r M&ؿgRz 5/g۲7ꄈ0}nn>mhjՠ.-;ބnߒ쯠jWʭ'4r%O̍?|(ݿVjC< %[6*Oi;ϕ<A= ֠2@HN7PhXhᲷgԪW_ nX/F9 6*5jʞk~#}Ii/H/ 10D,3jԩ+PN8NCכNNT썷^+P);NI>(!!yԞ ` ,XB 6 EEb!#M=  M `#ϫ%WKLC)/g2O?+3PBUڕ+͹@T jydɤY vPOP=:|!y5IE N"ׁ^PiĄIFe(B8(S*͘" !R! `bsK,rOVҧ~}t8?p"E8t 2"?ƶ*V{~+)Ν>Em426!v`3dtCocvfC<\pX Uan%GY/151E@g. F5ʑgA,]QNe6 5F)LJ(% ^6QhO40wK^xebH] NLePb4N ^{5;l<~ݾMG0MB)EL /0ja6m۬\GC7=%iS{W_"VӨaTx1d8k7 7Q6 = 8)'h6XJbRR'%K @,XJ v    £   `)KI@@Ă^x4@@,%`))؁Xp f ,%;pR NzlĂ`  NJbI/<   X@@@I @,8GA@@R 8)'6\4@@Ă-I( h80g/^DNXM3!rT[>0(Q[vOEq5ڛܴh/A:U? si ۷ %{.Չ#4bA -}}|?C~:9꠭D@@ :t8%ܴ6RDD$$|m!9j)@,%/hWy~Exm% I`/0h˓/[<-E@@,^. ..xPs!ͽ(ii}R">QBu{q#"//@'/~wKM+;Lw#M AA_ji2x,I)fʙLIgW]D?x@/)( /^ǏS?z7r/)"V8e" 9< ?ރN&cFԼ9Unْ׏l|8<G+^LG,J 9 gY($K>ٓmB #^p @(T#!Nק {hx.  营:*W}uL,@@l~6l}p WM<EQKkPt2X(FS4ͽT:{R-E@@qV ފB*=  ZKDN(RW~ ǏGo H\},E0d={B,=KBZ4-SItb!$ZNKI9])h"Z j@@Ă]p4@@XPK   d 삣   ĂZb'#d =8'h.%A@@@,8GsA@@@-`  NFb.8   j @,%{p2 Nv\PKbA-1؃Xp Z j@@Ă]p4@@XPK   d 삣   ĂZb'#d =8'h.%A@@@,8GsA@@@-`  NFb.8   j @,%{p2 Nv\PKUm؃BavN*'YR)I+ܮԮI+7h@^EP…eT@Ce_ѹW͛tmv%˕!Nw~Z:vRd(\ԨF m[J&!kd  EH0ѱs4xt9d~SJ7G6`!APy@Ə7);,Cm;@ ˗JF~B׭[ŋ)[LԦo_zY*B b@  7h E͛ŅKR(3{v##thioi3kh 98m +VcWSqӟB% "u3GB@<\  `!^^ԿsgiSCE OJ4[R-q~N]-{*Bo8~,bɸqT`AJ*J-KKEB< Mb!n>H" ?}J<ca~K IV ϒ!qCw ؈}"e3vmjҳOTY3P21?Lo0iشg5уPJ(m^LsqJɓ)s m1쓥Z5){%E@"XHD(n <ܰ ቅG,_bbѣL؟} v*nح},}| 7/o۳?}29eC-υhhDp|B,s~UPvObZ8x6;,VnB\/H  IA' 7gNXliҥ)I+dHJ WУ'O݇0sZbH#mZyc/E׬){_/o|cF7>"#!bXq%fW|ʗ(A~8)nu6if _  e7m;W)> ˰ca7XԗCo1d" .c@԰Q`V%`U( ?bjѻkvEz9!e",0h%g^ش._N~K0 'R޹WQqwo(Ҩ'{b3ZקwSnԮ *DLXde ԅ iQeV}Ζ+.cs<)bf'6gS;*O}T *}U4̺rot4Nv4a҇@ٿ_.Y"Fxŋq Cn bP~m$ok1M3wNnc''G{}Иx_ΞEPc6?v?y_C NjB @g z0Kg ljkA/h @2iv6뼁kٯw{dQmjHzP#}ؖ--o -b"X*=ӺR[" IDAT Vq`m[qkn,_M/ ,|Opk\_>z쭷2k͢O~Kz8Dom\orYrɧ@D|Rlb!kA DT(SmA%c7DSE._D#aQi"^묆L3̦Lm_E] &2J]`S xYeo6@`B@P x wrNz}5[u@c+9)k!i5_ !E uꦙ53O_]R|Dw{hfq7E,jДII-d$x>1sC7/i:eefbõ eEBg>al6-`/zR5fK; 1J$mkeh2Yl{ Kup%W{lhla<:֭pikL(iyfAv+nj:5 R]M5S^k6%Eu/yȒY",L/F:LTrqQg @=>jce#J ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ ?@@@ Z JJ*~55(%E X b%փh Z֒Y聕J D8ZZ_\Nb!,!/Ѳ#.iDD,%u]@@y}9$RuNb69a%m|u9 IIcYRP`7vc C|[~a,Er Hx%7{cI8!z E" cNJrMC@ p?\JDb9^!YmNUi'}XRExqѣRp 9d_\NbanB!(5m  Z+rPISYع܌\8@ݘwr(Nbdm7gm M̖6$ʶ_\vzX C`]4c-XHטlVwn]y63M(lz\9@݌(:{%g+8/.‰>IH`=>;Hj:1hJahq@ UV|i%5v ^]:š ݹ׼ZRsޙ=qB@7%`O=^?Q&+bLb`mI^X{X(o/4.纘 i6EY$ryYz@l"x n V-XO>XQ#ϱ9$aƾ}(I%-;~'5607Օ_"ZQvvA2d( 8!gA뚝 LX*ec %'ʒFVW9; ɐs aC,%G˴"ȉ =tAG6I3ϔk2C@)k.iiv"%+1Y¤}u6^d'9eyr~%pv|shf$q𭇿<ުUT0\]`V jIHhտLׂB+͓>@1}Oym^@OʍG^ͪ"2fX cbT{r=\滘^0lS``&ʠdI:C@y=YCrpk ~B@lԔs"yZSuKg/l[\L\ZvqXI+t}BbxH \ uUWĔ[ Jƍb /z"93Dm ؈&²B kz5?7܊h08dW;zk^ԭ(6q:oH7"* aMy7 '-{@D!m=Vv*|Ps"o.b2k{-ԁ ]5"ǫ/VƎLCuqi{9E^/ٟo,/pm|ךc&EBo{3$Z .g%%IBo @]TUVzBٟZ%lE݀۾nbʰPp>}݁@mm|wg*3 v¾:Vk^5 ܹ x%J Wo$v4v ~'9:z qkLϳu-`ÈSޯT+č'rw/7##1AV v΃,[.J 2f9BcpZ8VNC @ @l=$IENDB`slixmpp/docs/_static/images/from_&yet.png000066400000000000000000000053741477105560000210130ustar00rootroot00000000000000PNG  IHDROr1ztEXtSoftwareAdobe ImageReadyqe< IDATx TTe޹b̀D@@E(US1Z [RVY ߪD4$-U:@ A!m 1:`Re<kWf}ごxl=+**,z{:ۼxQ6@5>>To="SK$܋& Hu-Pug'tNߞ7Xk?Z9ԠX\1\Vlyfs/NYeh.Uaa. D0vg)+H'a$,NQN(prB&E<r5[9t;rZnmwI;#χszƣn/&[L˅R5U*C{Rf_=0o@_ӥ/=c8!`kn=le c䇼DMU@PG'Mݶ1QǏL֑V|CطHvdazl7@zn0-_ܐ}Իw:7(O[{B~ 6貸a3_ۦzAGhp㽯D/i ťg/NW,dA ɰ+~9-_jz乚||%BJ?x-8n}%*}9}EC}y r?ɤ±}i:EnΝa)|5%V2 P<׽sf+{J;f4iQz~ü!S(iVE[k_j6hVކ揯|[&S +0FSa]ahx8C g+}ס*lf͏)YTz7q( RIfWE~*]U@}oN^(Y 'A,KJؓ sUhuRYcY|_ҩ5 /ܻpݼ\Ƹ*$P LEuKƖn[;rK[Lp37Ϋ(.3`hhEOn}NCBq9Xzuhy"q>90*'#$iIمxj#Y(>QtjbB3U-m]]ŰZp0/j4jQa{y=9oGd:^-st6im2q6BGfamtx,&+|THl⚌P1L;=Q䇟x:?= :;t x\4rN'|F+c+w j"xqIr푻۾ U[YaQsa;;)lEiiTS ni<ϑmY!$T٨gxa7rʗpzIHNÓ~J|oɋp=漻wS('WtƐ-:6/Z9]-n(2sW1˸-sM_Y91\L/~Nծ^vz)j8rsT¯]1rڗ Fe@Gm2K2ĈPloV.?jXp~dhM9OaUwy$rTuK t. Cjh缆E*MY = >,qGN@ܣ;ozȑyؖeC63̘nCt}4uqVrvIr07pa(yCa'Pj +8Wb}~ahqz22Ѝ:)Q?kr2 ǻB<q'i+Q쿷T_ V5L8Zr,mk]Z\4 5oDcMб2 G`=]ɂi|yjZjxݑ=ꔃjJw< Ζ” 5xq`/2|Xy}m1"|?.w}uY~!Ur$ @az LY\dx"3,=ޑ) O!?x~=7ﮓ 0Xw9ҢIENDB`slixmpp/docs/api/000077500000000000000000000000001477105560000142605ustar00rootroot00000000000000slixmpp/docs/api/api.rst000066400000000000000000000052651477105560000155730ustar00rootroot00000000000000.. _internal-api: Internal "API" ============== Slixmpp has a generic API registry that can be used by its plugins to allow access control, redefinition of behaviour, without having to inherit from the plugin or do more dark magic. The idea is that each api call can be replaced, most of them use a form of in-memory storage that can be, for example, replaced with database or file-based storaged. Each plugin is assigned an API proxy bound to itself, but only a few make use of it. See also :ref:`api-simple-tuto`. Description of a generic API call ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python def get_toto(jid, node, ifrom, args): return 'toto' self.xmpp.plugin['xep_XXXX'].api.register(handler, 'get_toto') Each API call will receive 4 parameters (which can be ``None`` if data is not relevant to the operation), which are ``jid`` (``Optional[JID]``), ``node`` (``Optional[str]``), ``ifrom`` (``Optional[JID]``), and ``args`` (``Any``). - ``jid``, if relevant, represents the JID targeted by that operation - ``node``, if relevant is an arbitrary string, but was thought for, e.g., a pubsub or disco node. - ``ifrom``, if relevant, is the JID the event is coming from. - ``args`` is the event-specific data passed on by the plugin, often a dict of arguments (can be None as well). .. note:: Since 1.8.0, API calls can be coroutines. Handler hierarchy ~~~~~~~~~~~~~~~~~ The ``self.api.register()`` signature is as follows: .. code-block:: python def register(handler, op, jid=None, node=None, default=False): pass As you can see, :meth:`~.APIRegistry.register` takes an additional ctype parameter, but the :class:`~.APIWrapper` takes care of that for us (in most cases, it is the name of the XEP plugin, such as ``'xep_0XXX'``). When you register a handler, you register it for an ``op``, for **operation**. For example, ``get_vcard``. ``handler`` and ``op`` are the only two required parameters (and in many cases, all you will ever need). You can, however, go further and register handlers for specific values of the ``jid`` and ``node`` parameters of the calls. The priority of the execution of handlers is as follows: - Check if a handler for both values of ``node`` and ``jid`` has been defined - If not found, check if a handler for this value of ``jid`` has been defined - If not found, check if a handler for this value of ``node`` has been defined - If still not found, get the global handler (no parameter registered) Raw documentation ~~~~~~~~~~~~~~~~~ This documentation is provided for reference, but :meth:`~.APIRegistry.register` should be all you need. .. module:: slixmpp.api .. autoclass:: APIRegistry :members: .. autoclass:: APIWrapper slixmpp/docs/api/basexmpp.rst000066400000000000000000000001401477105560000166240ustar00rootroot00000000000000======== BaseXMPP ======== .. module:: slixmpp.basexmpp .. autoclass:: BaseXMPP :members: slixmpp/docs/api/clientxmpp.rst000066400000000000000000000001521477105560000171730ustar00rootroot00000000000000========== ClientXMPP ========== .. module:: slixmpp.clientxmpp .. autoclass:: ClientXMPP :members: slixmpp/docs/api/componentxmpp.rst000066400000000000000000000001711477105560000177200ustar00rootroot00000000000000============= ComponentXMPP ============= .. module:: slixmpp.componentxmpp .. autoclass:: ComponentXMPP :members: slixmpp/docs/api/exceptions.rst000066400000000000000000000002711477105560000171730ustar00rootroot00000000000000Exceptions ========== .. module:: slixmpp.exceptions .. autoexception:: XMPPError :members: .. autoexception:: IqError :members: .. autoexception:: IqTimeout :members: slixmpp/docs/api/index.rst000066400000000000000000000004071477105560000161220ustar00rootroot00000000000000API Reference ------------- .. toctree:: :maxdepth: 3 clientxmpp componentxmpp basexmpp exceptions xmlstream/jid xmlstream/stanzabase xmlstream/handler xmlstream/matcher xmlstream/xmlstream xmlstream/tostring api slixmpp/docs/api/plugins/000077500000000000000000000000001477105560000157415ustar00rootroot00000000000000slixmpp/docs/api/plugins/index.rst000066400000000000000000000026511477105560000176060ustar00rootroot00000000000000Plugin index ============ .. toctree:: :maxdepth: 2 xep_0004 xep_0009 xep_0012 xep_0013 xep_0020 xep_0027 xep_0030 xep_0033 xep_0045 xep_0047 xep_0049 xep_0050 xep_0054 xep_0055 xep_0059 xep_0060 xep_0065 xep_0066 xep_0070 xep_0071 xep_0077 xep_0079 xep_0080 xep_0082 xep_0084 xep_0085 xep_0086 xep_0092 xep_0100 xep_0106 xep_0107 xep_0108 xep_0115 xep_0118 xep_0122 xep_0128 xep_0131 xep_0133 xep_0152 xep_0153 xep_0163 xep_0172 xep_0184 xep_0186 xep_0191 xep_0196 xep_0198 xep_0199 xep_0202 xep_0203 xep_0221 xep_0222 xep_0223 xep_0224 xep_0231 xep_0235 xep_0249 xep_0256 xep_0257 xep_0258 xep_0264 xep_0279 xep_0280 xep_0292 xep_0297 xep_0300 xep_0308 xep_0313 xep_0317 xep_0319 xep_0332 xep_0333 xep_0334 xep_0335 xep_0352 xep_0353 xep_0356 xep_0359 xep_0363 xep_0369 xep_0372 xep_0377 xep_0380 xep_0382 xep_0385 xep_0394 xep_0402 xep_0403 xep_0404 xep_0405 xep_0410 xep_0421 xep_0422 xep_0424 xep_0425 xep_0428 xep_0437 xep_0439 xep_0441 xep_0444 xep_0446 xep_0447 xep_0461 xep_0469 xep_0482 xep_0490 xep_0492 slixmpp/docs/api/plugins/xep_0004.rst000066400000000000000000000006071477105560000177350ustar00rootroot00000000000000 XEP-0004: Data Forms ==================== .. module:: slixmpp.plugins.xep_0004 .. autoclass:: XEP_0004 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0004.stanza.field :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0004.stanza.form :members: :undoc-members: slixmpp/docs/api/plugins/xep_0009.rst000066400000000000000000000004561477105560000177440ustar00rootroot00000000000000 XEP-0009: Jabber-RPC ==================== .. module:: slixmpp.plugins.xep_0009 .. autoclass:: XEP_0009 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0009.stanza.RPC :members: :undoc-members: slixmpp/docs/api/plugins/xep_0012.rst000066400000000000000000000025741477105560000177410ustar00rootroot00000000000000 XEP-0012: Last Activity ======================= .. module:: slixmpp.plugins.xep_0012 .. autoclass:: XEP_0012 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0012: Internal API methods -------------------- This plugin uses an in-memory storage by default to keep track of the received and sent last activities. .. glossary:: get_last_activity - **jid**: :class:`~.JID` of whom to retrieve the last activity - **node**: unused - **ifrom**: who the request is from (None = local) - **args**: ``None`` or an :class:`~.Iq` that is requesting the - **returns** information. Get the last activity of a JID from the storage. set_last_activity - **jid**: :class:`~.JID` of whom to set the last activity - **node**: unused - **ifrom**: unused - **args**: A dict containing ``'seconds'`` and ``'status'`` ``{'seconds': Optional[int], 'status': Optional[str]}`` Set the last activity of a JID in the storage. del_last_activity - **jid**: :class:`~.JID` to delete from the storage - **node**: unused - **ifrom**: unused - **args**: unused Remove the last activity of a JID from the storage. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0012.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0013.rst000066400000000000000000000005321477105560000177320ustar00rootroot00000000000000 XEP-0013: Flexible Offline Message Retrieval ============================================ .. module:: slixmpp.plugins.xep_0013 .. autoclass:: XEP_0013 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0013.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0020.rst000066400000000000000000000004741477105560000177350ustar00rootroot00000000000000 XEP-0020: Feature Negotiation ============================= .. module:: slixmpp.plugins.xep_0020 .. autoclass:: XEP_0020 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0020.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0027.rst000066400000000000000000000025171477105560000177440ustar00rootroot00000000000000 XEP-0027: Current Jabber OpenPGP Usage ====================================== .. module:: slixmpp.plugins.xep_0027 .. autoclass:: XEP_0027 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0027: Internal API methods -------------------- The default API here is managing a JID→Keyid dict in-memory. .. glossary:: get_keyid - **jid**: :class:`~.JID` to get. - **node**: unused - **ifrom**: unused - **args**: unused - **returns**: ``Optional[str]``, the keyid or None Get the KeyiD for a JID, None if it is not found. set_keyid - **jid**: :class:`~.JID` to set the id for. - **node**: unused - **ifrom**: unused - **args**: ``str``, keyid to set Set the KeyiD for a JID. del_keyid - **jid**: :class:`~.JID` to delete from the mapping. - **node**: unused - **ifrom**: unused - **args**: unused Delete the KeyiD for a JID. get_keyids - **jid**: unused - **node**: unused - **ifrom**: unused - **args**: unused - **returns**: ``Dict[JID, str]`` the full internal mapping Get all currently stored KeyIDs. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0027.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0030.rst000066400000000000000000000014231477105560000177310ustar00rootroot00000000000000 XEP-0030: Service Discovery =========================== .. module:: slixmpp.plugins.xep_0030 .. autoclass:: XEP_0030 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0030: Internal API Methods -------------------- All ``api`` operations supported by the 0030 plugin are implemented as part of the :class:`~.StaticDisco` class which implement an in-memory cache for disco info and items. .. automodule:: slixmpp.plugins.xep_0030.static :members: :member-order: bysource Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0030.stanza.info :members: :member-order: bysource :undoc-members: .. automodule:: slixmpp.plugins.xep_0030.stanza.items :members: :member-order: bysource :undoc-members: slixmpp/docs/api/plugins/xep_0033.rst000066400000000000000000000005121477105560000177320ustar00rootroot00000000000000 XEP-0033: Extended Stanza Addressing ==================================== .. module:: slixmpp.plugins.xep_0033 .. autoclass:: XEP_0033 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0033.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0045.rst000066400000000000000000000005201477105560000177340ustar00rootroot00000000000000 XEP-0045: Multi-User Chat ========================= .. module:: slixmpp.plugins.xep_0045 .. autoclass:: XEP_0045 :member-order: bysource :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0045.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0047.rst000066400000000000000000000050171477105560000177440ustar00rootroot00000000000000 XEP-0047: In-band Bytestreams ============================= .. module:: slixmpp.plugins.xep_0047 .. autoclass:: XEP_0047 :members: :exclude-members: session_bind, plugin_init, plugin_end .. autoclass:: IBBytestream :members: .. _api-0047: Internal API methods -------------------- The API here is used to manage streams and authorize. The default handlers work with the config parameters. .. glossary:: authorized_sid (0047 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream is from. - **args**: :class:`~.Iq` of the stream request. - **returns**: ``True`` if the stream should be accepted, ``False`` otherwise. Check if the stream should be accepted. Uses the information setup by :term:`preauthorize_sid (0047 version)` by default. authorized (0047 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream is from. - **args**: :class:`~.Iq` of the stream request. - **returns**: ``True`` if the stream should be accepted, ``False`` otherwise. A fallback handler (run after :term:`authorized_sid (0047 version)`) to check if a stream should be accepted. Uses the ``auto_accept`` parameter by default. preauthorize_sid (0047 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream will be from. - **args**: Unused. Register a stream id to be accepted automatically (called from other plugins such as XEP-0095). get_stream - **jid**: :class:`~.JID` of local receiver. - **node**: stream id - **ifrom**: who the stream is from. - **args**: unused - **returns**: :class:`~.IBBytestream` Return a currently opened stream between two JIDs. set_stream - **jid**: :class:`~.JID` of local receiver. - **node**: stream id - **ifrom**: who the stream is from. - **args**: unused Register an opened stream between two JIDs. del_stream - **jid**: :class:`~.JID` of local receiver. - **node**: stream id - **ifrom**: who the stream is from. - **args**: unused Delete a stream between two JIDs. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0047.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0049.rst000066400000000000000000000004741477105560000177500ustar00rootroot00000000000000 XEP-0049: Private XML Storage ============================= .. module:: slixmpp.plugins.xep_0049 .. autoclass:: XEP_0049 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0049.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0050.rst000066400000000000000000000004641477105560000177370ustar00rootroot00000000000000 XEP-0050: Ad-Hoc Commands ========================= .. module:: slixmpp.plugins.xep_0050 .. autoclass:: XEP_0050 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0050.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0054.rst000066400000000000000000000022341477105560000177400ustar00rootroot00000000000000 XEP-0054: vcard-temp ==================== .. module:: slixmpp.plugins.xep_0054 .. autoclass:: XEP_0054 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0054: Internal API methods -------------------- This plugin maintains by default an in-memory cache of the received VCards. .. glossary:: set_vcard - **jid**: :class:`~.JID` of whom to set the vcard - **node**: unused - **ifrom**: unused - **args**: :class:`~.VCardTemp` object to store for this JID. Set a VCard for a JID. get_vcard - **jid**: :class:`~.JID` of whom to set the vcard - **node**: unused - **ifrom**: :class:`~.JID` the request is coming from - **args**: unused - **returns**: :class:`~.VCardTemp` object for this JID or None. Get a stored VCard for a JID. del_vcard - **jid**: :class:`~.JID` of whom to set the vcard - **node**: unused - **ifrom**: unused - **args**: unused Delete a stored VCard for a JID. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0054.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0055.rst000066400000000000000000000004601477105560000177400ustar00rootroot00000000000000 XEP-0055: Jabber search ======================= .. module:: slixmpp.plugins.xep_0055 .. autoclass:: XEP_0055 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0055.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0059.rst000066400000000000000000000006101477105560000177410ustar00rootroot00000000000000 XEP-0059: Result Set Management =============================== .. module:: slixmpp.plugins.xep_0059 .. autoclass:: XEP_0059 :members: :exclude-members: session_bind, plugin_init, plugin_end .. autoclass:: ResultIterator :members: :member-order: bysource Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0059.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0060.rst000066400000000000000000000012731477105560000177370ustar00rootroot00000000000000 XEP-0060: Publish-Subscribe =========================== .. module:: slixmpp.plugins.xep_0060 .. autoclass:: XEP_0060 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0060.stanza.base :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0060.stanza.pubsub :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0060.stanza.pubsub_errors :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0060.stanza.pubsub_owner :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0060.stanza.pubsub_event :members: :undoc-members: slixmpp/docs/api/plugins/xep_0065.rst000066400000000000000000000033541477105560000177460ustar00rootroot00000000000000 XEP-0065: SOCKS5 Bytestreams ============================ .. module:: slixmpp.plugins.xep_0065 .. autoclass:: XEP_0065 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0065: Internal API methods -------------------- The internal API is used here to authorize or pre-authorize streams. .. glossary:: authorized_sid (0065 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream is from. - **args**: :class:`~.Iq` of the stream request. - **returns**: ``True`` if the stream should be accepted, ``False`` otherwise. Check if the stream should be accepted. Uses the information setup by :term:`preauthorize_sid (0065 version)` by default. authorized (0065 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream is from. - **args**: :class:`~.Iq` of the stream request. - **returns**: ``True`` if the stream should be accepted, ``False`` otherwise. A fallback handler (run after :term:`authorized_sid (0065 version)`) to check if a stream should be accepted. Uses the ``auto_accept`` parameter by default. preauthorize_sid (0065 version) - **jid**: :class:`~.JID` receiving the stream initiation. - **node**: stream id - **ifrom**: who the stream will be from. - **args**: Unused. Register a stream id to be accepted automatically (called from other plugins such as XEP-0095). Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0065.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0066.rst000066400000000000000000000004661477105560000177500ustar00rootroot00000000000000 XEP-0066: Out of Band Data ========================== .. module:: slixmpp.plugins.xep_0066 .. autoclass:: XEP_0066 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0066.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0070.rst000066400000000000000000000005261477105560000177400ustar00rootroot00000000000000 XEP-0070: Verifying HTTP Requests via XMPP ========================================== .. module:: slixmpp.plugins.xep_0070 .. autoclass:: XEP_0070 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0070.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0071.rst000066400000000000000000000004461477105560000177420ustar00rootroot00000000000000 XEP-0071: XHTML-IM ================== .. module:: slixmpp.plugins.xep_0071 .. autoclass:: XEP_0071 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0071.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0077.rst000066400000000000000000000034311477105560000177450ustar00rootroot00000000000000 XEP-0077: In-Band Registration ============================== .. module:: slixmpp.plugins.xep_0077 .. autoclass:: XEP_0077 :members: :exclude-members: session_bind, plugin_init, plugin_end Internal APi methods -------------------- The API here is made to allow components to manage registered users. The default handlers make use of the plugin options and store users in memory. .. glossary:: user_get - **jid**: unused - **node**: unused - **ifrom**: who the request is coming from - **args**: :class:`~.Iq` registration request. - **returns**: ``dict`` containing user data or None. Get user data for a user. user_validate - **jid**: unused - **node**: unused - **ifrom**: who the request is coming from - **args**: :class:`~.Iq` registration request, 'register' payload. - **raises**: ValueError if some fields are invalid Validate form fields and save user data. user_remove - **jid**: unused - **node**: unused - **ifrom**: who the request is coming from - **args**: :class:`~.Iq` registration removal request. - **raises**: KeyError if the user is not found. Remove a user from the store. make_registration_form - **jid**: unused - **node**: unused - **ifrom**: who the request is coming from - **args**: :class:`~.Iq` registration request. - **raises**: KeyError if the user is not found. Return an :class:`~.Iq` reply for the request, with a form and options set. By default, use ``form_fields`` and ``form_instructions`` plugin config options. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0077.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0079.rst000066400000000000000000000005141477105560000177460ustar00rootroot00000000000000 XEP-0079: Advanced Message Processing ===================================== .. module:: slixmpp.plugins.xep_0079 .. autoclass:: XEP_0079 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0079.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0080.rst000066400000000000000000000004601477105560000177360ustar00rootroot00000000000000 XEP-0080: User Location ======================= .. module:: slixmpp.plugins.xep_0080 .. autoclass:: XEP_0080 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0080.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0082.rst000066400000000000000000000003261477105560000177410ustar00rootroot00000000000000 XEP-0082: XMPP Date and Time Profiles ===================================== .. module:: slixmpp.plugins.xep_0082 .. autoclass:: XEP_0082 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0084.rst000066400000000000000000000004541477105560000177450ustar00rootroot00000000000000 XEP-0084: User Avatar ===================== .. module:: slixmpp.plugins.xep_0084 .. autoclass:: XEP_0084 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0084.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0085.rst000066400000000000000000000005061477105560000177440ustar00rootroot00000000000000 XEP-0085: Chat State Notifications ================================== .. module:: slixmpp.plugins.xep_0085 .. autoclass:: XEP_0085 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0085.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0086.rst000066400000000000000000000005061477105560000177450ustar00rootroot00000000000000 XEP-0086: Error Condition Mappings ================================== .. module:: slixmpp.plugins.xep_0086 .. autoclass:: XEP_0086 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0086.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0092.rst000066400000000000000000000004661477105560000177470ustar00rootroot00000000000000 XEP-0092: Software Version ========================== .. module:: slixmpp.plugins.xep_0092 .. autoclass:: XEP_0092 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0092.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0100.rst000066400000000000000000000003061477105560000177260ustar00rootroot00000000000000 XEP-0100: Gateway interaction ============================= .. module:: slixmpp.plugins.xep_0100 .. autoclass:: XEP_0100 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0106.rst000066400000000000000000000002701477105560000177340ustar00rootroot00000000000000 XEP-0106: JID Escaping ====================== .. module:: slixmpp.plugins.xep_0106 .. autoclass:: XEP_0106 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0107.rst000066400000000000000000000004501477105560000177350ustar00rootroot00000000000000 XEP-0107: User Mood =================== .. module:: slixmpp.plugins.xep_0107 .. autoclass:: XEP_0107 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0107.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0108.rst000066400000000000000000000004601477105560000177370ustar00rootroot00000000000000 XEP-0108: User Activity ======================= .. module:: slixmpp.plugins.xep_0108 .. autoclass:: XEP_0108 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0108.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0115.rst000066400000000000000000000033511477105560000177370ustar00rootroot00000000000000 XEP-0115: Entity Capabilities ============================= .. module:: slixmpp.plugins.xep_0115 .. autoclass:: XEP_0115 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0115: Internal API methods -------------------- This internal API extends the Disco internal API, and also manages an in-memory cache of verstring→disco info, and fulljid→verstring. .. glossary:: cache_caps - **jid**: unused - **node**: unused - **ifrom**: unused - **args**: a ``dict`` containing the verstring and :class:`~.DiscoInfo` payload ( ``{'verstring': Optional[str], 'info': Optional[DiscoInfo]}``) Cache a verification string with its payload. get_caps - **jid**: JID to retrieve the verstring for (unused with the default handler) - **node**: unused - **ifrom**: unused - **args**: a ``dict`` containing the verstring ``{'verstring': str}`` - **returns**: The :class:`~.DiscoInfo` payload for that verstring. Get a disco payload from a verstring. assign_verstring - **jid**: :class:`~.JID` (full) to assign the verstring to - **node**: unused - **ifrom**: unused - **args**: a ``dict`` containing the verstring ``{'verstring': str}`` Cache JID→verstring information. get_verstring - **jid**: :class:`~.JID` to use for fetching the verstring - **node**: unused - **ifrom**: unused - **args**: unused - **returns**: ``str``, the verstring Retrieve a verstring for a JID. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0115.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0118.rst000066400000000000000000000004501477105560000177370ustar00rootroot00000000000000 XEP-0118: User Tune =================== .. module:: slixmpp.plugins.xep_0118 .. autoclass:: XEP_0118 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0118.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0122.rst000066400000000000000000000005001477105560000177260ustar00rootroot00000000000000 XEP-0122: Data Forms Validation =============================== .. module:: slixmpp.plugins.xep_0122 .. autoclass:: XEP_0122 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0122.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0128.rst000066400000000000000000000021631477105560000177430ustar00rootroot00000000000000 XEP-0128: Service Discovery Extensions ====================================== .. module:: slixmpp.plugins.xep_0128 .. autoclass:: XEP_0128 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0128: Internal API methods -------------------- .. glossary:: add_extended_info - **jid**: JID to set the extended info for - **node**: note to set the info at - **ifrom**: unused - **args**: A :class:`~.Form` or list of forms to add to the disco extended info for this JID/node. Add extended info for a JID/node. set_extended_info - **jid**: JID to set the extended info for - **node**: note to set the info at - **ifrom**: unused - **args**: A :class:`~.Form` or list of forms to set as the disco extended info for this JID/node. Set extended info for a JID/node. del_extended_info - **jid**: JID to delete the extended info from - **node**: note to delete the info from - **ifrom**: unused - **args**: unused Delete extended info for a JID/node. slixmpp/docs/api/plugins/xep_0131.rst000066400000000000000000000005361477105560000177370ustar00rootroot00000000000000 XEP-0131: Stanza Headers and Internet Metadata ============================================== .. module:: slixmpp.plugins.xep_0131 .. autoclass:: XEP_0131 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0131.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0133.rst000066400000000000000000000003141477105560000177330ustar00rootroot00000000000000 XEP-0133: Service Administration ================================ .. module:: slixmpp.plugins.xep_0133 .. autoclass:: XEP_0133 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0152.rst000066400000000000000000000005021477105560000177330ustar00rootroot00000000000000 XEP-0152: Reachability Addresses ================================ .. module:: slixmpp.plugins.xep_0152 .. autoclass:: XEP_0152 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0152.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0153.rst000066400000000000000000000024421477105560000177410ustar00rootroot00000000000000 XEP-0153: vCard-Based Avatars ============================= .. module:: slixmpp.plugins.xep_0153 .. autoclass:: XEP_0153 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0153: Internal API methods -------------------- The internal API is used here to maintain an in-memory JID→avatar hash cache. .. glossary:: set_hash - **jid**: :class:`~.JID` of whom to retrieve the last activity - **node**: unused - **ifrom**: unused - **args**: ``str``, avatar hash Set the avatar hash for a JID. reset_hash - **jid**: :class:`~.JID` of whom to retrieve the last activity - **node**: unused - **ifrom**: :class:`~.JID` of the entity requesting the reset. - **args**: unused - **returns** information. Reset the avatar hash for a JID. This downloads the vcard and computes the hash. get_hash - **jid**: :class:`~.JID` of whom to retrieve the last activity - **node**: unused - **ifrom**: unused - **args**: unused - **returns**: ``Optional[str]``, the avatar hash Get the avatar hash for a JID. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0153.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0163.rst000066400000000000000000000003401477105560000177350ustar00rootroot00000000000000 XEP-0163: Personal Eventing Protocol (PEP) ========================================== .. module:: slixmpp.plugins.xep_0163 .. autoclass:: XEP_0163 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0172.rst000066400000000000000000000004601477105560000177400ustar00rootroot00000000000000 XEP-0172: User Nickname ======================= .. module:: slixmpp.plugins.xep_0172 .. autoclass:: XEP_0172 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0172.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0184.rst000066400000000000000000000005101477105560000177370ustar00rootroot00000000000000 XEP-0184: Message Delivery Receipts =================================== .. module:: slixmpp.plugins.xep_0184 .. autoclass:: XEP_0184 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0184.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0186.rst000066400000000000000000000004701477105560000177460ustar00rootroot00000000000000 XEP-0186: Invisible Command =========================== .. module:: slixmpp.plugins.xep_0186 .. autoclass:: XEP_0186 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0186.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0191.rst000066400000000000000000000004661477105560000177470ustar00rootroot00000000000000 XEP-0191: Blocking Command ========================== .. module:: slixmpp.plugins.xep_0191 .. autoclass:: XEP_0191 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0191.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0196.rst000066400000000000000000000004541477105560000177510ustar00rootroot00000000000000 XEP-0196: User Gaming ===================== .. module:: slixmpp.plugins.xep_0196 .. autoclass:: XEP_0196 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0196.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0198.rst000066400000000000000000000004701477105560000177510ustar00rootroot00000000000000 XEP-0198: Stream Management =========================== .. module:: slixmpp.plugins.xep_0198 .. autoclass:: XEP_0198 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0198.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0199.rst000066400000000000000000000004501477105560000177500ustar00rootroot00000000000000 XEP-0199: XMPP Ping =================== .. module:: slixmpp.plugins.xep_0199 .. autoclass:: XEP_0199 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0199.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0202.rst000066400000000000000000000004541477105560000177350ustar00rootroot00000000000000 XEP-0202: Entity Time ===================== .. module:: slixmpp.plugins.xep_0202 .. autoclass:: XEP_0202 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0202.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0203.rst000066400000000000000000000004661477105560000177410ustar00rootroot00000000000000 XEP-0203: Delayed Delivery ========================== .. module:: slixmpp.plugins.xep_0203 .. autoclass:: XEP_0203 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0203.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0221.rst000066400000000000000000000005061477105560000177340ustar00rootroot00000000000000 XEP-0221: Data Forms Media Element ================================== .. module:: slixmpp.plugins.xep_0221 .. autoclass:: XEP_0221 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0221.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0222.rst000066400000000000000000000003701477105560000177340ustar00rootroot00000000000000 XEP-0222: Persistent Storage of Public Data via PubSub ====================================================== .. module:: slixmpp.plugins.xep_0222 .. autoclass:: XEP_0222 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0223.rst000066400000000000000000000003721477105560000177370ustar00rootroot00000000000000 XEP-0223: Persistent Storage of Private Data via PubSub ======================================================= .. module:: slixmpp.plugins.xep_0223 .. autoclass:: XEP_0223 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0224.rst000066400000000000000000000004501477105560000177350ustar00rootroot00000000000000 XEP-0224: Attention =================== .. module:: slixmpp.plugins.xep_0224 .. autoclass:: XEP_0224 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0224.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0231.rst000066400000000000000000000022561477105560000177410ustar00rootroot00000000000000 XEP-0231: Bits of Binary ======================== .. module:: slixmpp.plugins.xep_0231 .. autoclass:: XEP_0231 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0231: Internal API methods -------------------- The default API handlers for this plugin manage an in-memory cache of bits of binary by content-id. .. glossary:: set_bob - **jid**: :class:`~.JID` sending the bob - **node**: unused - **ifrom**: :class:`~JID` receiving the bob - **args**: :class:`~.BitsOfBinary` element. Set a BoB in the cache. get_bob - **jid**: :class:`~.JID` receiving the bob - **node**: unused - **ifrom**: :class:`~JID` sending the bob - **args**: ``str`` content-id of the bob - **returns**: :class:`~.BitsOfBinary` element. Get a BoB from the cache. del_bob - **jid**: unused - **node**: unused - **ifrom**: :class:`~JID` sending the bob - **args**: ``str`` content-id of the bob Delete a BoB from the cache. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0231.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0235.rst000066400000000000000000000004641477105560000177440ustar00rootroot00000000000000 XEP-0235: OAuth Over XMPP ========================= .. module:: slixmpp.plugins.xep_0235 .. autoclass:: XEP_0235 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0235.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0249.rst000066400000000000000000000005021477105560000177420ustar00rootroot00000000000000 XEP-0249: Direct MUC Invitations ================================ .. module:: slixmpp.plugins.xep_0249 .. autoclass:: XEP_0249 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0249.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0256.rst000066400000000000000000000003221477105560000177400ustar00rootroot00000000000000 XEP-0256: Last Activity in Presence =================================== .. module:: slixmpp.plugins.xep_0256 .. autoclass:: XEP_0256 :members: :exclude-members: session_bind, plugin_init, plugin_end slixmpp/docs/api/plugins/xep_0257.rst000066400000000000000000000005641477105560000177510ustar00rootroot00000000000000 XEP-0257: Client Certificate Management for SASL EXTERNAL ========================================================= .. module:: slixmpp.plugins.xep_0257 .. autoclass:: XEP_0257 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0257.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0258.rst000066400000000000000000000005041477105560000177440ustar00rootroot00000000000000 XEP-0258: Security Labels in XMPP ================================= .. module:: slixmpp.plugins.xep_0258 .. autoclass:: XEP_0258 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0258.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0264.rst000066400000000000000000000005101477105560000177360ustar00rootroot00000000000000 XEP-0264: Jingle Content Thumbnails =================================== .. module:: slixmpp.plugins.xep_0264 .. autoclass:: XEP_0264 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0264.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0279.rst000066400000000000000000000004641477105560000177540ustar00rootroot00000000000000 XEP-0279: Server IP Check ========================= .. module:: slixmpp.plugins.xep_0279 .. autoclass:: XEP_0279 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0279.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0280.rst000066400000000000000000000004641477105560000177440ustar00rootroot00000000000000 XEP-0280: Message Carbons ========================= .. module:: slixmpp.plugins.xep_0280 .. autoclass:: XEP_0280 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0280.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0292.rst000066400000000000000000000004471477105560000177500ustar00rootroot00000000000000 XEP-0292: vCard4 Over XMPP ========================== .. module:: slixmpp.plugins.xep_0292 .. autoclass:: XEP_0292 :members: :exclude-members: plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0292.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0297.rst000066400000000000000000000004701477105560000177510ustar00rootroot00000000000000 XEP-0297: Stanza Forwarding =========================== .. module:: slixmpp.plugins.xep_0297 .. autoclass:: XEP_0297 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0297.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0300.rst000066400000000000000000000005541477105560000177350ustar00rootroot00000000000000 XEP-0300: Use of Cryptographic Hash Functions in XMPP ===================================================== .. module:: slixmpp.plugins.xep_0300 .. autoclass:: XEP_0300 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0300.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0308.rst000066400000000000000000000005041477105560000177400ustar00rootroot00000000000000 XEP-0308: Last Message Correction ================================= .. module:: slixmpp.plugins.xep_0308 .. autoclass:: XEP_0308 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0308.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0313.rst000066400000000000000000000005461477105560000177420ustar00rootroot00000000000000 XEP-0313: Message Archive Management ==================================== .. module:: slixmpp.plugins.xep_0313 .. autoclass:: XEP_0313 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0313.stanza :members: :member-order: bysource :undoc-members: slixmpp/docs/api/plugins/xep_0317.rst000066400000000000000000000004361477105560000177440ustar00rootroot00000000000000 XEP-0317: Hats ============== .. module:: slixmpp.plugins.xep_0317 .. autoclass:: XEP_0317 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0317.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0319.rst000066400000000000000000000016721477105560000177510ustar00rootroot00000000000000 XEP-0319: Last User Interaction in Presence =========================================== .. module:: slixmpp.plugins.xep_0319 .. autoclass:: XEP_0319 :members: :exclude-members: session_bind, plugin_init, plugin_end .. _api-0319: Internal API methods -------------------- The default API manages an in-memory cache of idle periods. .. glossary:: set_idle - **jid**: :class:`~.JID` who has been idling - **node**: unused - **ifrom**: unused - **args**: :class:`datetime`, timestamp of the idle start Set the idle start for a JID. get_idle - **jid**: :class:`~.JID` to get the idle time of - **node**: unused - **ifrom**: unused - **args**: : unused - **returns**: :class:`datetime` Get the idle start timestamp for a JID. Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0319.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0332.rst000066400000000000000000000010021477105560000177270ustar00rootroot00000000000000 XEP-0332: HTTP over XMPP transport ================================== .. module:: slixmpp.plugins.xep_0332 .. autoclass:: XEP_0332 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0332.stanza.data :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0332.stanza.request :members: :undoc-members: .. automodule:: slixmpp.plugins.xep_0332.stanza.response :members: :undoc-members: slixmpp/docs/api/plugins/xep_0333.rst000066400000000000000000000004561477105560000177440ustar00rootroot00000000000000 XEP-0333: Chat Markers ====================== .. module:: slixmpp.plugins.xep_0333 .. autoclass:: XEP_0333 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0333.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0334.rst000066400000000000000000000005061477105560000177410ustar00rootroot00000000000000 XEP-0334: Message Processing Hints ================================== .. module:: slixmpp.plugins.xep_0334 .. autoclass:: XEP_0334 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0334.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0335.rst000066400000000000000000000004641477105560000177450ustar00rootroot00000000000000 XEP-0335: JSON Containers ========================= .. module:: slixmpp.plugins.xep_0335 .. autoclass:: XEP_0335 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0335.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0352.rst000066400000000000000000000005041477105560000177370ustar00rootroot00000000000000 XEP-0352: Client State Indication ================================= .. module:: slixmpp.plugins.xep_0352 .. autoclass:: XEP_0352 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0352.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0353.rst000066400000000000000000000005101477105560000177350ustar00rootroot00000000000000 XEP-0353: Jingle Message Initiation =================================== .. module:: slixmpp.plugins.xep_0353 .. autoclass:: XEP_0353 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0353.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0356.rst000066400000000000000000000004671477105560000177530ustar00rootroot00000000000000 XEP-0356: Privileged Entity =========================== .. module:: slixmpp.plugins.xep_0356 .. autoclass:: XEP_0356 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0356.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0359.rst000066400000000000000000000005161477105560000177510ustar00rootroot00000000000000 XEP-0359: Unique and Stable Stanza IDs ====================================== .. module:: slixmpp.plugins.xep_0359 .. autoclass:: XEP_0359 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0359.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0363.rst000066400000000000000000000006211477105560000177410ustar00rootroot00000000000000 XEP-0363: HTTP File Upload ========================== .. module:: slixmpp.plugins.xep_0363 .. autoclass:: XEP_0363 :members: :exclude-members: session_bind, plugin_init, plugin_end .. autoclass:: UploadServiceNotFound .. autoclass:: FileTooBig .. autoclass:: HTTPError Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0363.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0369.rst000066400000000000000000000004461477105560000177540ustar00rootroot00000000000000 XEP-0369: MIX-CORE ================== .. module:: slixmpp.plugins.xep_0369 .. autoclass:: XEP_0369 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0369.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0372.rst000066400000000000000000000004521477105560000177430ustar00rootroot00000000000000 XEP-0372: References ==================== .. module:: slixmpp.plugins.xep_0372 .. autoclass:: XEP_0372 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0372.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0377.rst000066400000000000000000000004621477105560000177510ustar00rootroot00000000000000 XEP-0377: Spam Reporting ======================== .. module:: slixmpp.plugins.xep_0377 .. autoclass:: XEP_0377 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0377.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0380.rst000066400000000000000000000005141477105560000177410ustar00rootroot00000000000000 XEP-0380: Explicit Message Encryption ===================================== .. module:: slixmpp.plugins.xep_0380 .. autoclass:: XEP_0380 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0380.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0382.rst000066400000000000000000000004661477105560000177510ustar00rootroot00000000000000 XEP-0382: Spoiler Messages ========================== .. module:: slixmpp.plugins.xep_0382 .. autoclass:: XEP_0382 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0382.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0385.rst000066400000000000000000000005401477105560000177450ustar00rootroot00000000000000 XEP-0385: Stateless Inline Media Sharing (SIMS) =============================================== .. module:: slixmpp.plugins.xep_0385 .. autoclass:: XEP_0385 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0385.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0394.rst000066400000000000000000000004621477105560000177500ustar00rootroot00000000000000 XEP-0394: Message Markup ======================== .. module:: slixmpp.plugins.xep_0394 .. autoclass:: XEP_0394 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0394.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0402.rst000066400000000000000000000004761477105560000177430ustar00rootroot00000000000000 XEP-0402: PEP Native Bookmarks ============================== .. module:: slixmpp.plugins.xep_0402 .. autoclass:: XEP_0402 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0402.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0403.rst000066400000000000000000000004561477105560000177420ustar00rootroot00000000000000 XEP-0403: MIX-Presence ====================== .. module:: slixmpp.plugins.xep_0403 .. autoclass:: XEP_0403 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0403.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0404.rst000066400000000000000000000004461477105560000177420ustar00rootroot00000000000000 XEP-0404: MIX-ANON ================== .. module:: slixmpp.plugins.xep_0404 .. autoclass:: XEP_0404 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0404.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0405.rst000066400000000000000000000004441477105560000177410ustar00rootroot00000000000000 XEP-0405: MIX-PAM ================= .. module:: slixmpp.plugins.xep_0405 .. autoclass:: XEP_0405 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0405.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0410.rst000066400000000000000000000005111477105560000177300ustar00rootroot00000000000000 XEP-0410: MUC Self-Ping (Schrödinger's Chat) ============================================ .. module:: slixmpp.plugins.xep_0410 .. autoclass:: XEP_0410 :members: :exclude-members: session_bind, plugin_init, plugin_end, _update_ping_results, _handle_condition, _on_muc_activity .. autoclass:: PingStatus :members: slixmpp/docs/api/plugins/xep_0421.rst000066400000000000000000000005621477105560000177400ustar00rootroot00000000000000 XEP-0421: Anonymous unique occupant identifiers for MUCs ======================================================== .. module:: slixmpp.plugins.xep_0421 .. autoclass:: XEP_0421 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0421.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0422.rst000066400000000000000000000004701477105560000177370ustar00rootroot00000000000000 XEP-0422: Message Fastening =========================== .. module:: slixmpp.plugins.xep_0422 .. autoclass:: XEP_0422 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0422.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0424.rst000066400000000000000000000004721477105560000177430ustar00rootroot00000000000000 XEP-0424: Message Retraction ============================ .. module:: slixmpp.plugins.xep_0424 .. autoclass:: XEP_0424 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0424.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0425.rst000066400000000000000000000004721477105560000177440ustar00rootroot00000000000000 XEP-0425: Message Moderation ============================ .. module:: slixmpp.plugins.xep_0425 .. autoclass:: XEP_0425 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0425.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0428.rst000066400000000000000000000004741477105560000177510ustar00rootroot00000000000000 XEP-0428: Fallback Indication ============================= .. module:: slixmpp.plugins.xep_0428 .. autoclass:: XEP_0428 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0428.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0437.rst000066400000000000000000000005061477105560000177450ustar00rootroot00000000000000 XEP-0437: Room Activity Indicators ================================== .. module:: slixmpp.plugins.xep_0437 .. autoclass:: XEP_0437 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0437.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0439.rst000066400000000000000000000004621477105560000177500ustar00rootroot00000000000000 XEP-0439: Quick Response ======================== .. module:: slixmpp.plugins.xep_0439 .. autoclass:: XEP_0439 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0439.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0441.rst000066400000000000000000000005421477105560000177400ustar00rootroot00000000000000 XEP-0441: Message Archive Management Preferences ================================================ .. module:: slixmpp.plugins.xep_0441 .. autoclass:: XEP_0441 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0441.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0444.rst000066400000000000000000000004701477105560000177430ustar00rootroot00000000000000 XEP-0444: Message Reactions =========================== .. module:: slixmpp.plugins.xep_0444 .. autoclass:: XEP_0444 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0444.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0446.rst000066400000000000000000000005001477105560000177370ustar00rootroot00000000000000 XEP-0446: File metadata element =============================== .. module:: slixmpp.plugins.xep_0446 .. autoclass:: XEP_0446 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0446.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0447.rst000066400000000000000000000005021477105560000177420ustar00rootroot00000000000000 XEP-0447: Stateless File Sharing ================================ .. module:: slixmpp.plugins.xep_0447 .. autoclass:: XEP_0447 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0447.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0461.rst000066400000000000000000000004641477105560000177450ustar00rootroot00000000000000 XEP-0461: Message Replies ========================= .. module:: slixmpp.plugins.xep_0461 .. autoclass:: XEP_0461 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0461.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0469.rst000066400000000000000000000004651477105560000177560ustar00rootroot00000000000000XEP-0469: Bookmark Pinning ========================== .. module:: slixmpp.plugins.xep_0469 .. autoclass:: XEP_0469 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0469.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0482.rst000066400000000000000000000004561477105560000177510ustar00rootroot00000000000000 XEP-0482: Call Invites ====================== .. module:: slixmpp.plugins.xep_0482 .. autoclass:: XEP_0482 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0482.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0490.rst000066400000000000000000000005301477105560000177410ustar00rootroot00000000000000 XEP-0490: Message Displayed Synchronization =========================================== .. module:: slixmpp.plugins.xep_0490 .. autoclass:: XEP_0490 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0490.stanza :members: :undoc-members: slixmpp/docs/api/plugins/xep_0492.rst000066400000000000000000000005121477105560000177430ustar00rootroot00000000000000 XEP-0492: Chat Notification Settings ==================================== .. module:: slixmpp.plugins.xep_0492 .. autoclass:: XEP_0492 :members: :exclude-members: session_bind, plugin_init, plugin_end Stanza elements --------------- .. automodule:: slixmpp.plugins.xep_0492.stanza :members: :undoc-members: slixmpp/docs/api/stanza/000077500000000000000000000000001477105560000155605ustar00rootroot00000000000000slixmpp/docs/api/stanza/index.rst000066400000000000000000000001521477105560000174170ustar00rootroot00000000000000 Core Stanzas ------------ .. toctree:: :maxdepth: 2 rootstanza message presence iq slixmpp/docs/api/stanza/iq.rst000066400000000000000000000001401477105560000167160ustar00rootroot00000000000000IQ Stanza ========= .. module:: slixmpp.stanza :noindex: .. autoclass:: Iq :members: slixmpp/docs/api/stanza/message.rst000066400000000000000000000001561477105560000177400ustar00rootroot00000000000000Message Stanza ============== .. module:: slixmpp.stanza :noindex: .. autoclass:: Message :members: slixmpp/docs/api/stanza/presence.rst000066400000000000000000000001441477105560000201150ustar00rootroot00000000000000Presence Stanza =============== .. module:: slixmpp.stanza .. autoclass:: Presence :members: slixmpp/docs/api/stanza/rootstanza.rst000066400000000000000000000001511477105560000205130ustar00rootroot00000000000000Root Stanza =========== .. module:: slixmpp.stanza.rootstanza .. autoclass:: RootStanza :members: slixmpp/docs/api/xmlstream/000077500000000000000000000000001477105560000162745ustar00rootroot00000000000000slixmpp/docs/api/xmlstream/handler.rst000066400000000000000000000006031477105560000204420ustar00rootroot00000000000000Stanza Handlers =============== The Basic Handler ----------------- .. module:: slixmpp.xmlstream.handler.base .. autoclass:: BaseHandler :members: Callback -------- .. module:: slixmpp.xmlstream.handler .. autoclass:: Callback :members: CoroutineCallback ----------------- .. autoclass:: CoroutineCallback :members: Waiter ------ .. autoclass:: Waiter :members: slixmpp/docs/api/xmlstream/jid.rst000066400000000000000000000001361477105560000175740ustar00rootroot00000000000000Jabber IDs (JID) ================= .. module:: slixmpp.jid .. autoclass:: JID :members: slixmpp/docs/api/xmlstream/matcher.rst000066400000000000000000000011311477105560000204450ustar00rootroot00000000000000Stanza Matchers =============== The Basic Matcher ----------------- .. module:: slixmpp.xmlstream.matcher.base .. autoclass:: MatcherBase :members: ID Matching ----------- .. module:: slixmpp.xmlstream.matcher.id .. autoclass:: MatcherId :members: Stanza Path Matching -------------------- .. module:: slixmpp.xmlstream.matcher.stanzapath .. autoclass:: StanzaPath :members: XPath ----- .. module:: slixmpp.xmlstream.matcher.xpath .. autoclass:: MatchXPath :members: XMLMask ------- .. module:: slixmpp.xmlstream.matcher.xmlmask .. autoclass:: MatchXMLMask :members: slixmpp/docs/api/xmlstream/stanzabase.rst000066400000000000000000000103431477105560000211620ustar00rootroot00000000000000.. _stanzabase: ============== Stanza Objects ============== .. module:: slixmpp.xmlstream.stanzabase The :mod:`~slixmpp.xmlstream.stanzabase` module provides a wrapper for the standard :mod:`~xml.etree.ElementTree` module that makes working with XML less painful. Instead of having to manually move up and down an element tree and insert subelements and attributes, you can interact with an object that behaves like a normal dictionary or JSON object, which silently maps keys to XML attributes and elements behind the scenes. Overview -------- The usefulness of this layer grows as the XML you have to work with becomes nested. The base unit here, :class:`ElementBase`, can map to a single XML element, or several depending on how advanced of a mapping is desired from interface keys to XML structures. For example, a single :class:`ElementBase` derived class could easily describe: .. code-block:: xml Hi! Custom item 1 Custom item 2 Custom item 3 If that chunk of XML were put in the :class:`ElementBase` instance ``msg``, we could extract the data from the XML using:: >>> msg['extra'] ['Custom item 1', 'Custom item 2', 'Custom item 3'] Provided we set up the handler for the ``'extra'`` interface to load the ```` element content into a list. The key concept is that given an XML structure that will be repeatedly used, we can define a set of :term:`interfaces` which when we read from, write to, or delete, will automatically manipulate the underlying XML as needed. In addition, some of these interfaces may in turn reference child objects which expose interfaces for particularly complex child elements of the original XML chunk. .. seealso:: :ref:`create-stanza-interfaces`. Because the :mod:`~slixmpp.xmlstream.stanzabase` module was developed as part of an `XMPP `_ library, these chunks of XML are referred to as :term:`stanzas `, and in Slixmpp we refer to a subclass of :class:`ElementBase` which defines the interfaces needed for interacting with a given :term:`stanza` a :term:`stanza object`. To make dealing with more complicated and nested :term:`stanzas ` or XML chunks easier, :term:`stanza objects ` can be composed in two ways: as iterable child objects or as plugins. Iterable child stanzas, or :term:`substanzas `, are accessible through a special ``'substanzas'`` interface. This option is useful for stanzas which may contain more than one of the same kind of element. When there is only one child element, the plugin method is more useful. For plugins, a parent stanza object delegates one of its XML child elements to the plugin stanza object. Here is an example: .. code-block:: xml We can can arrange this stanza into two objects: an outer, wrapper object for dealing with the ```` element and its attributes, and a plugin object to control the ```` payload element. If we give the plugin object the name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then we can access the plugin as so:: >>> iq['disco_info'] ' ' We can then drill down through the plugin object's interfaces as desired:: >>> iq['disco_info']['identities'] [('client', 'bot', 'Slixmpp Bot')] Plugins may also add new interfaces to the parent stanza object as if they had been defined by the parent directly, and can also override the behaviour of an interface defined by the parent. .. seealso:: - :ref:`create-stanza-plugins` - :ref:`is_extension` - :ref:`overrides` Registering Stanza Plugins -------------------------- .. autofunction:: register_stanza_plugin ElementBase ----------- .. autoclass:: ElementBase :members: :private-members: :special-members: StanzaBase ---------- .. autoclass:: StanzaBase :members: slixmpp/docs/api/xmlstream/tostring.rst000066400000000000000000000035771477105560000207130ustar00rootroot00000000000000.. module:: slixmpp.xmlstream.tostring :noindex: .. _tostring: XML Serialization ================= Since the XML layer of Slixmpp is based on :mod:`~xml.etree.ElementTree`, why not just use the built-in :func:`~xml.etree.ElementTree.tostring` method? The answer is that using that method produces ugly results when using namespaces. The :func:`tostring()` method used here intelligently hides namespaces when able and does not introduce excessive namespace prefixes:: >>> from slixmpp.xmlstream.tostring import tostring >>> from xml.etree import ElementTree as ET >>> xml = ET.fromstring('') >>> ET.tostring(xml) '' >>> tostring(xml) '' As a side effect of this namespace hiding, using :func:`tostring()` may produce unexpected results depending on how the :func:`tostring()` method is invoked. For example, when sending XML on the wire, the main XMPP stanzas with their namespace of ``jabber:client`` will not include the namespace because that is already declared by the stream header. But, if you create a :class:`~slixmpp.stanza.message.Message` instance and dump it to the terminal, the ``jabber:client`` namespace will appear. .. autofunction:: slixmpp.xmlstream.tostring Escaping Special Characters --------------------------- In order to prevent errors when sending arbitrary text as the textual content of an XML element, certain characters must be escaped. These are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping mechanism is to replace those characters with their equivalent escape entities: ``&``, ``<``, ``>``, ``'``, and ``"``. In the future, the use of CDATA sections may be allowed to reduce the size of escaped text or for when other XMPP processing agents do not undertand these entities. .. autofunction:: xml_escape slixmpp/docs/api/xmlstream/xmlstream.rst000066400000000000000000000001621477105560000210410ustar00rootroot00000000000000========== XML Stream ========== .. module:: slixmpp.xmlstream.xmlstream .. autoclass:: XMLStream :members: slixmpp/docs/architecture.rst000066400000000000000000000126121477105560000167250ustar00rootroot00000000000000.. index:: XMLStream, BaseXMPP, ClientXMPP, ComponentXMPP Slixmpp Architecture ====================== The core of Slixmpp is contained in four classes: ``XMLStream``, ``BaseXMPP``, ``ClientXMPP``, and ``ComponentXMPP``. Along side this stack is a library for working with XML objects that eliminates most of the tedium of creating/manipulating XML. .. image:: _static/images/arch_layers.png :height: 300px :align: center .. index:: XMLStream The Foundation: XMLStream ------------------------- :class:`~slixmpp.xmlstream.xmlstream.XMLStream` is a mostly XMPP-agnostic class whose purpose is to read and write from a bi-directional XML stream. It also allows for callback functions to execute when XML matching given patterns is received; these callbacks are also referred to as :term:`stream handlers `. The class also provides a basic eventing system which can be triggered either manually or on a timed schedule. The event loop ~~~~~~~~~~~~~~ :class:`~slixmpp.xmlstream.xmlstream.XMLStream` instances inherit the :class:`asyncio.BaseProtocol` class, and therefore do not have to handle reads and writes directly, but receive data through :meth:`~slixmpp.xmlstream.xmlstream.XMLStream.data_received` and write data in the socket transport. Upon receiving data, :term:`stream handlers ` are run immediately, except if they are coroutines, in which case they are scheduled using :meth:`asyncio.async`. :term:`Event handlers ` (which are called inside :term:`stream handlers `) work the same way. How XML Text is Turned into Action ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To demonstrate the flow of information, let's consider what happens when this bit of XML is received (with an assumed namespace of ``jabber:client``): .. code-block:: xml Hej! #. **Convert XML strings into objects.** Incoming text is parsed and converted into XML objects (using ElementTree) which are then wrapped into what are referred to as :term:`Stanza objects `. The appropriate class for the new object is determined using a map of namespaced element names to classes. Our incoming XML is thus turned into a :class:`~slixmpp.stanza.Message` :term:`stanza object` because the namespaced element name ``{jabber:client}message`` is associated with the class :class:`~slixmpp.stanza.Message`. #. **Match stanza objects to callbacks.** These objects are then compared against the stored patterns associated with the registered callback handlers. Each handler matching our :term:`stanza object` is then added to a list. #. **Processing callbacks** Every handler in the list is then called with the :term:`stanza object` as a parameter; if the handler is a :class:`~slixmpp.xmlstream.handler.CoroutineCallback` then it will be scheduled in the event loop using :meth:`asyncio.async` instead of run. #. **Raise Custom Events** Since a :term:`stream handler` shouldn't block, if extensive processing for a stanza is required (such as needing to send and receive an :class:`~slixmpp.stanza.Iq` stanza), then custom events must be used. These events are not explicitly tied to the incoming XML stream and may be raised at any time. In contrast to :term:`stream handlers `, these functions are referred to as :term:`event handlers `. The code for :meth:`BaseXMPP._handle_message` follows this pattern, and raises a ``'message'`` event .. code-block:: python self.event('message', msg) #. **Process Custom Events** The :term:`event handlers ` are then executed, passing the stanza as the only argument. .. note:: Events may be raised without needing :term:`stanza objects `. For example, you could use ``self.event('custom', {'a': 'b'})``. You don't even need any arguments: ``self.event('no_parameters')``. However, every event handler MUST accept at least one argument. Finally, after a long trek, our message is handed off to the user's custom handler in order to do awesome stuff:: reply = msg.reply() reply['body'] = "Hey! This is awesome!" reply.send() .. index:: BaseXMPP, XMLStream Raising XMPP Awareness: BaseXMPP -------------------------------- While :class:`~slixmpp.xmlstream.xmlstream.XMLStream` attempts to shy away from anything too XMPP specific, :class:`~slixmpp.basexmpp.BaseXMPP`'s sole purpose is to provide foundational support for sending and receiving XMPP stanzas. This support includes registering the basic message, presence, and iq stanzas, methods for creating and sending stanzas, and default handlers for incoming messages and keeping track of presence notifications. The plugin system for adding new XEP support is also maintained by :class:`~slixmpp.basexmpp.BaseXMPP`. .. index:: ClientXMPP, BaseXMPP ClientXMPP ---------- :class:`~slixmpp.clientxmpp.ClientXMPP` extends :class:`~slixmpp.clientxmpp.BaseXMPP` with additional logic for connecting to an XMPP server by performing DNS lookups. It also adds support for stream features such as STARTTLS and SASL. .. index:: ComponentXMPP, BaseXMPP ComponentXMPP ------------- :class:`~slixmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of :class:`~slixmpp.basexmpp.BaseXMPP` that implements the component handshake protocol. slixmpp/docs/conf.py000066400000000000000000000166761477105560000150260ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Slixmpp documentation build configuration file, created by # sphinx-quickstart on Tue Aug 9 22:27:06 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # get version automagically from source tree from slixmpp.version import __version__ as version release = ".".join(version.split(".")[0:2]) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinx_autodoc_typehints', ] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Slixmpp' year = datetime.datetime.now().year copyright = u'{}, Mathieu Pasquet, Maxime Buquet, Emmanuel Gil Peyrot, Florent Le Coz'.format(year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # auto imported from code! # The short X.Y version. # version = '1.4' # The full version, including alpha/beta/rc tags. # release = '1.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'tango' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {'headingcolor': '#CFCFCF', 'linkcolor': '#4A7389'} # 00ADEE # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'slixmpp' # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = '%s Documentation' % release # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = { } # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Slixmppdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Slixmpp.tex', u'Slixmpp Documentation', u'Nathan Fritz, Lance Stout', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'slixmpp', u'Slixmpp Documentation', [u'Mathieu Pasquet, Maxime Buquet, Emmanuel Gil Peyrot, Florent Le Coz'], 1) ] intersphinx_mapping = {'python': ('http://docs.python.org/3.9', 'python-objects.inv')} slixmpp/docs/differences.rst000066400000000000000000000033031477105560000165150ustar00rootroot00000000000000.. _differences: Differences from SleekXMPP ========================== **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 edits the stanza object should make its own copy. **Replies** Because stanzas are not copied anymore, :meth:`Stanza.reply() <.StanzaBase.reply>` calls (for :class:`IQs <.Iq>`, :class:`Messages <.Message>`, etc) now return a new object instead of editing the stanza object in-place. **Block and threaded arguments** All the functions that had a ``threaded=`` or ``block=`` argument do not have it anymore. Also, :meth:`.Iq.send` **does not block anymore**. **Coroutine facilities** **See** :ref:`using_asyncio` If an event handler is a coroutine, it will be called asynchronously in the event loop instead of inside the event caller. A CoroutineCallback class has been added to create coroutine stream handlers, which will be also handled in the event loop. The :class:`~.slixmpp.stanza.Iq` object’s :meth:`~.slixmpp.stanza.Iq.send` method now **always** return a :class:`~.asyncio.Future` which result will be set to the IQ reply when it is received, or to ``None`` if the IQ is not of type ``get`` or ``set``. Many plugins (WIP) calls which retrieve information also return the same future. **Architectural differences** slixmpp does not have an event queue anymore, and instead processes handlers directly after receiving the XML stanza. .. note:: If you find something that doesn’t work but should, please report it. slixmpp/docs/event_index.rst000066400000000000000000000467371477105560000165720ustar00rootroot00000000000000Event Index =========== Slixmpp relies on events and event handlers to act on received data from the server. Some of those events come from the very base of Slixmpp such as :class:`~.BaseXMPP` or :class:`~.XMLStream`, while most of them are emitted from plugins which add their own listeners. There are often multiple events running for a single stanza, with different levels of granularity, so code must take care of not processing the same stanza twice. .. glossary:: :sorted: connected - **Data:** ``{}`` - **Source:** :py:class:`~.xmlstream.XMLstream` Signal that a connection has been made with the XMPP server, but a session has not yet been established. connection_failed - **Data:** ``{}`` or ``Failure Stanza`` if available - **Source:** :py:class:`~.xmlstream.XMLstream` Signal that a connection can not be established after number of attempts. changed_status - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.roster.item.RosterItem` Triggered when a presence stanza is received from a JID with a show type different than the last presence stanza from the same JID. changed_subscription - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` Triggered whenever a presence stanza with a type of ``subscribe``, ``subscribed``, ``unsubscribe``, or ``unsubscribed`` is received. Note that if the values ``xmpp.auto_authorize`` and ``xmpp.auto_subscribe`` are set to ``True`` or ``False``, and not ``None``, then will either accept or reject all subscription requests before your event handlers are called. Set these values to ``None`` if you wish to make more complex subscription decisions. chatstate_active - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0085` When a message containing an ```` chatstate is received. chatstate_composing - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0085` When a message containing a ```` chatstate is received. chatstate_gone - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0085` When a message containing a ```` chatstate is received. chatstate_inactive - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0085` When a message containing an ```` chatstate is received. chatstate_paused - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0085` When a message containing a ```` chatstate is received. disco_info - **Data:** :py:class:`~.DiscoInfo` - **Source:** :py:class:`~.disco.XEP_0030` Triggered whenever a ``disco#info`` result stanza is received. disco_items - **Data:** :py:class:`~.DiscoItems` - **Source:** :py:class:`~.disco.XEP_0030` Triggered whenever a ``disco#items`` result stanza is received. disconnected - **Data:** ``Union[str, Exception]``, the reason for the disconnect (if any). If a textual reason is not provided and an exception is the cause, it will be given to the event handler. - **Source:** :py:class:`~.XMLstream` Signal that the connection with the XMPP server has been lost. failed_auth - **Data:** ``{}`` - **Source:** :py:class:`~.ClientXMPP`, :py:class:`~.XEP_0078` Signal that the server has rejected the provided login credentials. gmail_notify - **Data:** ``{}`` - **Source:** :py:class:`~.plugins.gmail_notify.gmail_notify` Signal that there are unread emails for the Gmail account associated with the current XMPP account. gmail_messages - **Data:** :py:class:`~.Iq` - **Source:** :py:class:`~.plugins.gmail_notify.gmail_notify` Signal that there are unread emails for the Gmail account associated with the current XMPP account. got_online - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.roster.item.RosterItem` If a presence stanza is received from a JID which was previously marked as offline, and the presence has a show type of '``chat``', '``dnd``', '``away``', or '``xa``', then this event is triggered as well. got_offline - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.roster.item.RosterItem` Signal that an unavailable presence stanza has been received from a JID. groupchat_invite - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0045` When a Mediated MUC invite is received. groupchat_direct_invite - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0249` When a Direct MUC invite is received. groupchat_message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0045` Triggered whenever a message is received from a multi-user chat room. groupchat_presence - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0045` Triggered whenever a presence stanza is received from a user in a multi-user chat room. groupchat_subject - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0045` Triggered whenever the subject of a multi-user chat room is changed, or announced when joining a room. killed - **Data:** ``{}`` - **Source:** :class:`~.XMLStream` When the stream is aborted. message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`BaseXMPP <.BaseXMPP>` Makes the contents of message stanzas that include tags available whenever one is received. Be sure to check the message type to handle error messages appropriately. message_error - **Data:** :py:class:`~.Message` - **Source:** :py:class:`BaseXMPP <.BaseXMPP>` Makes the contents of message stanzas available whenever one is received. Only handler messages with an ``error`` type. message_form - **Data:** :py:class:`~.Form` - **Source:** :py:class:`~.XEP_0004` Currently the same as :term:`message_xform`. message_xform - **Data:** :py:class:`~.Form` - **Source:** :py:class:`~.XEP_0004` Triggered whenever a data form is received inside a message. muc::[room]::got_offline - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. Triggered whenever we receive an unavailable presence from a MUC occupant. muc::[room]::got_online - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. Triggered whenever we receive a presence from a MUC occupant we do not have in the local cache. muc::[room]::message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. Triggered whenever we receive a message from a MUC we are in. muc::[room]::presence - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. muc::[room]::self-presence - **Data:** :class:`~.Presence` - **Source:** :class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. Triggered whenever we receive a presence with status code ``110`` (for example on MUC join, or nick change). muc::[room]::presence-error - **Data:** :class:`~.Presence` - **Source:** :class:`~.XEP_0045` - **Name parameters:** ``room``, the room this is coming from. Triggered whenever we receive a presence of ``type="error"`` from a MUC. presence_available - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``available``' is received. presence_error - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``error``' is received. presence_form - **Data:** :py:class:`~.Form` - **Source:** :py:class:`~.XEP_0004` This event is present in the XEP-0004 plugin code, but is currently not used. presence_probe - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``probe``' is received. presence_subscribe - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``subscribe``' is received. presence_subscribed - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``subscribed``' is received. presence_unavailable - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``unavailable``' is received. presence_unsubscribe - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``unsubscribe``' is received. presence_unsubscribed - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.BaseXMPP` A presence stanza with a type of '``unsubscribed``' is received. roster_update - **Data:** :py:class:`~.Roster` - **Source:** :py:class:`~.ClientXMPP` An IQ result containing roster entries is received. sent_presence - **Data:** ``{}`` - **Source:** :py:class:`~.roster.multi.Roster` Signal that an initial presence stanza has been written to the XML stream. session_end - **Data:** ``{}`` - **Source:** :py:class:`~.xmlstream.XMLstream` Signal that a connection to the XMPP server has been lost and the current stream session has ended. Equivalent to :term:`disconnected`, unless the `XEP-0198: Stream Management `_ plugin is loaded. Plugins that maintain session-based state should clear themselves when this event is fired. session_start - **Data:** ``{}`` - **Source:** :py:class:`.ClientXMPP`, :py:class:`~.ComponentXMPP`, :py:class:`~.XEP-0078` Signal that a connection to the XMPP server has been made and a session has been established. session_resumed - **Data:** ``{}`` - **Source:** :class:`~.XEP_0198` When Stream Management manages to resume an ongoing session after reconnecting. socket_error - **Data:** ``Socket`` exception object - **Source:** :py:class:`~.xmlstream.XMLstream` stream_error - **Data:** :py:class:`~.StreamError` - **Source:** :py:class:`~.BaseXMPP` reactions - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0444` When a message containing reactions is received. carbon_received - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0280` When a carbon for a received message is received. carbon_sent - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0280` When a carbon for a sent message (from another of our resources) is received. marker - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0333` Whenever a chat marker is received (any of them). marker_received - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0333` Whenever a ```` chat marker is received. marker_displayed - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0333` Whenever a ```` chat marker is received. marker_acknowledged - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0333` Whenever an ```` chat marker is received. attention - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0224` Whenever a message containing an attention payload is received. message_correction - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0308` Whenever a message containing a correction is received. receipt_received - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0184` Whenever a message receipt is received. jingle_message_propose - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0353` jingle_message_retract - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0353` jingle_message_accept - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0353` jingle_message_proceed - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0353` jingle_message_reject - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0353` room_activity - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0437` When a room activity stanza is received by a client. room_activity_bare - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0437` When an empty room activity stanza is received (typically by a component). sm_enabled - **Data:** :py:class:`~.stanza.Enabled` - **Source:** :py:class:`~.XEP_0198` When Stream Management is successfully enabled. sm_disabled - **Data:** ``{}`` - **Source:** :py:class:`~.XEP_0198` When Stream Management gets disabled (when disconnected). ibb_stream_start - **Data:** :py:class:`~.stream.IBBBytestream` - **Source:** :py:class:`~.XEP_0047` When a stream is successfully opened with a remote peer. ibb_stream_end - **Data:** :py:class:`~.stream.IBBBytestream` - **Source:** :py:class:`~.XEP_0047` When an opened stream closes. ibb_stream_data - **Data:** :py:class:`~.stream.IBBBytestream` - **Source:** :py:class:`~.XEP_0047` When data is received on an opened stream. stream:[stream id]:[peer jid] - **Data:** :py:class:`~.stream.IBBBytestream` - **Source:** :py:class:`~.XEP_0047` - **Name parameters:** ``stream id``, the id of the stream, and ``peer jid`` the JID of the entity the stream is established with. When a stream is opened (with specific sid and jid parameters). command - **Data:** :py:class:`~.Iq` - **Source:** :py:class:`~.XEP_0050` When an ad-hoc command is received. command_[action] - **Data:** :py:class:`~.Iq` - **Source:** :py:class:`~.XEP_0050` - **Name parameters:** ``action``, the action referenced in the command payload. When a command with the specific action is received. pubsub_publish - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``publish`` is received. pubsub_retract - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``retract`` is received. pubsub_purge - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``purge`` is received. pubsub_delete - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``delete`` is received. pubsub_config - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``config`` is received. pubsub_subscription - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``subscription`` is received. call_invite - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0482` call_retract - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0482` call_reject - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0482` call_leave - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0482` call_left - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0482` muc_ping_changed - **Data:** ``dict(key: Tuple[JID, JID], previous: PingStatus, result: PingStatus)`` - **Source:** :py:class:`~.XEP_0410` legacy_login - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0100` legacy_logout - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0100` legacy_presence_unavailable - **Data:** :py:class:`~.Presence` - **Source:** :py:class:`~.XEP_0100` legacy_message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0100` gateway_message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0100` moderated_message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0425` retracted_message - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0424` Dedicated PubSub Events ======================= The :class:`~.XEP_0060` plugin (and :class:`~.XEP_0163` plugin, which uses the former) allows other plugins to map specific namespaces in PubSub notifications to a dedicated name prefix. The current list of plugin prefixes is the following: - ``bookmarks``: :class:`~.XEP_0048` - ``user_location``: :class:`~.XEP_0080` - ``avatar_metadata``: :class:`~.XEP_0084` - ``avatar_data``: :class:`~.XEP_0084` - ``user_mood``: :class:`~.XEP_0107` - ``user_activity``: :class:`~.XEP_0108` - ``user_tune``: :class:`~.XEP_0118` - ``reachability``: :class:`~.XEP_0152` - ``user_nick``: :class:`~.XEP_0172` - ``user_gaming``: :class:`~.XEP_0196` - ``mix_participant_info``: :class:`~.XEP_0369` - ``mix_channel_info``: :class:`~.XEP_0369` .. glossary:: :sorted: [plugin]_publish - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``publish`` is received. [plugin]_retract - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``retract`` is received. [plugin]_purge - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``purge`` is received. [plugin]_delete - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``delete`` is received. [plugin]_config - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``config`` is received. [plugin]_subscription - **Data:** :py:class:`~.Message` - **Source:** :py:class:`~.XEP_0060` When a pubsub event of type ``subscription`` is received. slixmpp/docs/getting_started/000077500000000000000000000000001477105560000166765ustar00rootroot00000000000000slixmpp/docs/getting_started/component.rst000066400000000000000000000043211477105560000214320ustar00rootroot00000000000000.. _echocomponent: ================================= Create and Run a Server Component ================================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version with `Git `_. Many XMPP applications eventually graduate to requiring to run as a server component in order to meet scalability requirements. To demonstrate how to turn an XMPP client bot into a component, we'll turn the echobot example (:ref:`echobot`) into a component version. The first difference is that we will add an additional import statement: .. code-block:: python from slixmpp.componentxmpp import ComponentXMPP Likewise, we will change the bot's class definition to match: .. code-block:: python class EchoComponent(ComponentXMPP): def __init__(self, jid, secret, server, port): ComponentXMPP.__init__(self, jid, secret, server, port) A component instance requires two extra parameters compared to a client instance: ``server`` and ``port``. These specifiy the name and port of the XMPP server that will be accepting the component. For example, for a MUC component, the following could be used: .. code-block:: python muc = ComponentXMPP('muc.slixmpp.com', '******', 'slixmpp.com', 5555) .. note:: The ``server`` value is **NOT** derived from the provided JID for the component, unlike with client connections. One difference with the component version is that we do not have to handle the :term:`session_start` event if we don't wish to deal with presence. The other, main difference with components is that the ``'from'`` value for every stanza must be explicitly set, since components may send stanzas from multiple JIDs. To do so, the :meth:`~slixmpp.basexmpp.BaseXMPP.send_message()` and :meth:`~slixmpp.basexmpp.BaseXMPP.send_presence()` accept the parameters ``mfrom`` and ``pfrom``, respectively. For any method that uses :class:`~slixmpp.stanza.iq.Iq` stanzas, ``ifrom`` may be used. Final Product ------------- .. include:: ../../examples/echo_component.py :literal: slixmpp/docs/getting_started/echobot.rst000066400000000000000000000331151477105560000210560ustar00rootroot00000000000000.. _echobot: =============================== Slixmpp Quickstart - Echo Bot =============================== .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version with `Git `_. As a basic starting project, we will create an echo bot which will reply to any messages sent to it. We will also go through adding some basic command line configuration for enabling or disabling debug log outputs and setting the username and password for the bot. For the command line options processing, we will use the built-in ``argparse`` module and the ``getpass`` module for reading in passwords. TL;DR Just Give Me the Code --------------------------- As you wish: :ref:`the completed example `. Overview -------- To get started, here is a brief outline of the structure that the final project will have: .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp '''Here we will create out echo bot class''' if __name__ == '__main__': '''Here we will configure and read command line options''' '''Here we will instantiate our echo bot''' '''Finally, we connect the bot and start listening for messages''' Creating the EchoBot Class -------------------------- There are three main types of entities within XMPP — servers, components, and clients. Since our echo bot will only be responding to a few people, and won't need to remember thousands of users, we will use a client connection. A client connection is the same type that you use with your standard IM client such as Pidgin or Psi. Slixmpp comes with a :class:`ClientXMPP ` class which we can extend to add our message echoing feature. :class:`ClientXMPP ` requires the parameters ``jid`` and ``password``, so we will let our ``EchoBot`` class accept those as well. .. code-block:: python class EchoBot(slixmpp.ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) Handling Session Start ~~~~~~~~~~~~~~~~~~~~~~ The XMPP spec requires clients to broadcast its presence and retrieve its roster (buddy list) once it connects and establishes a session with the XMPP server. Until these two tasks are completed, some servers may not deliver or send messages or presence notifications to the client. So we now need to be sure that we retrieve our roster and send an initial presence once the session has started. To do that, we will register an event handler for the :term:`session_start` event. .. code-block:: python def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.start) Since we want the method ``self.start`` to execute when the :term:`session_start` event is triggered, we also need to define the ``self.start`` handler. .. code-block:: python async def start(self, event): self.send_presence() await self.get_roster() .. warning:: Not sending an initial presence and retrieving the roster when using a client instance can prevent your program from receiving presence notifications or messages depending on the XMPP server you have chosen. Our event handler, like every event handler, accepts a single parameter which typically is the stanza that was received that caused the event. In this case, ``event`` will just be an empty dictionary since there is no associated data. Our first task of sending an initial presence is done using :meth:`send_presence `. Calling :meth:`send_presence ` without any arguments will send the simplest stanza allowed in XMPP: .. code-block:: xml The second requirement is fulfilled using :meth:`get_roster `, which will send an IQ stanza requesting the roster to the server and then wait for the response. You may be wondering what :meth:`get_roster ` returns since we are not saving any return value. The roster data is saved by an internal handler to ``self.roster``, and in the case of a :class:`ClientXMPP ` instance to ``self.client_roster``. (The difference between ``self.roster`` and ``self.client_roster`` is that ``self.roster`` supports storing roster information for multiple JIDs, which is useful for components, whereas ``self.client_roster`` stores roster data for just the client's JID.) It is possible for a timeout to occur while waiting for the server to respond, which can happen if the network is excessively slow or the server is no longer responding. In that case, an :class:`IQTimeout ` is raised. Similarly, an :class:`IQError ` exception can be raised if the request contained bad data or requested the roster for the wrong user. In either case, you can wrap the ``get_roster()`` call in a ``try``/``except`` block to retry the roster retrieval process. The XMPP stanzas from the roster retrieval process could look like this: .. code-block:: xml Additionally, since :meth:`get_roster ` is using ```` stanzas, which will always receive an answer, it should be awaited on, to keep a synchronous flow. Responding to Messages ~~~~~~~~~~~~~~~~~~~~~~ Now that an ``EchoBot`` instance handles :term:`session_start`, we can begin receiving and responding to messages. Now we can register a handler for the :term:`message` event that is raised whenever a messsage is received. .. code-block:: python def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.start) self.add_event_handler('message', self.message) The :term:`message` event is fired whenever a ```` stanza is received, including for group chat messages, errors, etc. Properly responding to messages thus requires checking the ``'type'`` interface of the message :term:`stanza object`. For responding to only messages addressed to our bot (and not from a chat room), we check that the type is either ``normal`` or ``chat``. (Other potential types are ``error``, ``headline``, and ``groupchat``.) .. code-block:: python def message(self, msg): if msg['type'] in ('normal', 'chat'): msg.reply("Thanks for sending:\n%s" % msg['body']).send() Let's take a closer look at the ``.reply()`` method used above. For message stanzas, ``.reply()`` accepts the parameter ``body`` (also as the first positional argument), which is then used as the value of the ```` element of the message. Setting the appropriate ``to`` JID is also handled by ``.reply()``. Another way to have sent the reply message would be to use :meth:`send_message `, which is a convenience method for generating and sending a message based on the values passed to it. If we were to use this method, the above code would look as so: .. code-block:: python def message(self, msg): if msg['type'] in ('normal', 'chat'): self.send_message(mto=msg['from'], mbody='Thanks for sending:\n%s' % msg['body']) Whichever method you choose to use, the results in action will look like this: .. code-block:: xml Hej! Thanks for sending: Hej! .. note:: XMPP does not require stanzas sent by a client to include a ``from`` attribute, and leaves that responsibility to the XMPP server. However, if a sent stanza does include a ``from`` attribute, it must match the full JID of the client or some servers will reject it. Slixmpp thus leaves out the ``from`` attribute when replying using a client connection. Command Line Arguments and Logging ---------------------------------- While this isn't part of Slixmpp itself, we do want our echo bot program to be able to accept a JID and password from the command line instead of hard coding them. We will use the ``argparse`` module for this. We want to accept three parameters: the JID for the echo bot, its password, and a flag for displaying the debugging logs. We also want these to be optional parameters, since passing a password directly through the command line can be a security risk. .. code-block:: python if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=EchoBot.__doc__) # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") Since we included a flag for enabling debugging logs, we need to configure the ``logging`` module to behave accordingly. .. code-block:: python if __name__ == '__main__': # .. option parsing from above .. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') Connecting to the Server and Processing --------------------------------------- There are three steps remaining until our echo bot is complete: 1. We need to instantiate the bot. 2. The bot needs to connect to an XMPP server. 3. We have to instruct the bot to start running and processing messages. Creating the bot is straightforward, but we can also perform some configuration at this stage. For example, let's say we want our bot to support `service discovery `_ and `pings `_: .. code-block:: python if __name__ == '__main__': # .. option parsing and logging steps from above xmpp = EchoBot(opts.jid, opts.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0199') # Ping If the ``EchoBot`` class had a hard dependency on a plugin, we could register that plugin in the ``EchoBot.__init__`` method instead. Now we're ready to connect and begin echoing messages. If you have the package ``aiodns`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP.connect` method will perform a DNS query to find the appropriate server to connect to for the given JID. If you do not have ``aiodns``, then Slixmpp will attempt to connect to the hostname used by the JID, unless an address tuple is supplied to :meth:`slixmpp.clientxmpp.ClientXMPP.connect`. .. code-block:: python if __name__ == '__main__': # .. option parsing & echo bot configuration xmpp.connect(): asyncio.get_event_loop().run_forever() The :meth:`slixmpp.basexmpp.BaseXMPP.connect` will only schedule a connection asynchronously. To actually connect, you need to let the event loop take over. This is done the normal asyncio way, which you can learn about in the `official Python documentation `_. Here we are making it run forever, but you can use any asyncio handling you want, for instance to integrate slixmpp into an existing event loop. Another common usecase is to make it run only until it gets disconnected, with ``asyncio.get_event_loop().run_until_complete(xmpp.disconnected)``. .. note:: In previous versions of slixmpp, there was a ``process()`` method which handled the event loop for you, but it was a very common source of confusion for people unfamiliar with asyncio. .. _echobot_complete: The Final Product ----------------- Here then is what the final result should look like after working through the guide above. The code can also be found in the Slixmpp `examples directory `_. .. compound:: You can run the code using: .. code-block:: sh python echobot.py -d -j echobot@example.com which will prompt for the password and then begin echoing messages. To test, open your regular IM client and start a chat with the echo bot. Messages you send to it should be mirrored back to you. Be careful if you are using the same JID for the echo bot that you also have logged in with another IM client. Messages could be routed to your IM client instead of the bot. .. include:: ../../examples/echo_client.py :literal: slixmpp/docs/getting_started/index.rst000066400000000000000000000002631477105560000205400ustar00rootroot00000000000000Getting Started (with examples) ------------------------------- .. toctree:: :maxdepth: 3 echobot sendlogout component presence muc scheduler iq slixmpp/docs/getting_started/iq.rst000066400000000000000000000132271477105560000200460ustar00rootroot00000000000000Send/Receive IQ Stanzas ======================= Unlike :class:`~slixmpp.stanza.message.Message` and :class:`~slixmpp.stanza.presence.Presence` stanzas which only use text data for basic usage, :class:`~slixmpp.stanza.Iq` stanzas require using XML payloads, and generally entail creating a new Slixmpp plugin to provide the necessary convenience methods to make working with them easier. Basic Use --------- XMPP's use of :class:`~slixmpp.stanza.Iq` stanzas is built around namespaced ```` elements. For clients, just sending the empty ```` element will suffice for retrieving information. For example, a very basic implementation of service discovery would just need to be able to send: .. code-block:: xml Creating Iq Stanzas ~~~~~~~~~~~~~~~~~~~ Slixmpp provides built-in support for creating basic :class:`~slixmpp.stanza.Iq` stanzas this way. The relevant methods are: * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_get` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_set` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_result` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_error` * :meth:`~slixmpp.basexmpp.BaseXMPP.make_iq_query` These methods all follow the same pattern: create or modify an existing :class:`~slixmpp.stanza.Iq` stanza, set the ``'type'`` value based on the method name, and finally add a ```` element with the given namespace. For example, to produce the query above, you would use: .. code-block:: python self.make_iq_get(queryxmlns='http://jabber.org/protocol/disco#info', ito='user@example.com') Sending Iq Stanzas ~~~~~~~~~~~~~~~~~~ Once an :class:`~slixmpp.stanza.Iq` stanza is created, sending it over the wire is done using its :meth:`~slixmpp.stanza.Iq.send()` method, like any other stanza object. However, there are a few extra options to control how to wait for the query's response, as well as how to handle the result. :meth:`~slixmpp.stanza.Iq.send()` returns an :class:`~asyncio.Future` object, which can be awaited on until a ``result`` is received. These options are: * ``timeout``: When using the blocking behaviour, the call will eventually timeout with an error. The default timeout is 30 seconds, but this may be overidden two ways. To change the timeout globally, set: .. code-block:: python self.response_timeout = 10 To change the timeout for a single call, the ``timeout`` parameter works: .. code-block:: python iq.send(timeout=60) * ``callback``: When not using a blocking call, using the ``callback`` argument is a simple way to register a handler that will execute whenever a response is finally received. .. code-block:: python iq.send(callback=self.a_callback) .. note:: ``callback`` can be effectively replaced using ``await``, and standard exception handling (see below), which provide a more linear and readable workflow. Properly working with :class:`~slixmpp.stanza.Iq` stanzas requires handling the intended, normal flow, error responses, and timed out requests. To make this easier, two exceptions may be thrown by :meth:`~slixmpp.stanza.Iq.send()`: :exc:`~slixmpp.exceptions.IqError` and :exc:`~slixmpp.exceptions.IqTimeout`. These exceptions only apply to the default, blocking calls. .. code-block:: python try: resp = await iq.send() # ... do stuff with expected Iq result except IqError as e: err_resp = e.iq # ... handle error case except IqTimeout: # ... no response received in time pass If you do not care to distinguish between errors and timeouts, then you can combine both cases with a generic :exc:`~slixmpp.exceptions.XMPPError` exception: .. code-block:: python try: resp = await iq.send() except XMPPError: # ... Don't care about the response pass Advanced Use ------------ Going beyond the basics provided by Slixmpp requires building at least a rudimentary Slixmpp plugin to create a :term:`stanza object` for interfacting with the :class:`~slixmpp.stanza.Iq` payload. .. seealso:: * :ref:`create-plugin` * :ref:`work-with-stanzas` * :ref:`using-handlers-matchers` The typical way to respond to :class:`~slixmpp.stanza.Iq` requests is to register stream handlers. As an example, suppose we create a stanza class named ``CustomXEP`` which uses the XML element ````, and has a :attr:`~slixmpp.xmlstream.stanzabase.ElementBase.plugin_attrib` value of ``custom_xep``. There are two types of incoming :class:`~slixmpp.stanza.Iq` requests: ``get`` and ``set``. You can register a handler that will accept both and then filter by type as needed, as so: .. code-block:: python self.register_handler(Callback( 'CustomXEP Handler', StanzaPath('iq/custom_xep'), self._handle_custom_iq)) # ... def _handle_custom_iq(self, iq): if iq['type'] == 'get': # ... pass elif iq['type'] == 'set': # ... pass else: # ... This will capture error responses too pass If you want to filter out query types beforehand, you can adjust the matching filter by using ``@type=get`` or ``@type=set`` if you are using the recommended :class:`~slixmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher. .. code-block:: python self.register_handler(Callback( 'CustomXEP Handler', StanzaPath('iq@type=get/custom_xep'), self._handle_custom_iq_get)) # ... def _handle_custom_iq_get(self, iq): assert(iq['type'] == 'get') slixmpp/docs/getting_started/muc.rst000066400000000000000000000152371477105560000202240ustar00rootroot00000000000000.. _mucbot: ========================= Multi-User Chat (MUC) Bot ========================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. If you have not yet installed Slixmpp, do so now by either checking out a version from `Git `_. Now that you've got the basic gist of using Slixmpp by following the echobot example (:ref:`echobot`), we can use one of the bundled plugins to create a very popular XMPP starter project: a `Multi-User Chat`_ (MUC) bot. Our bot will login to an XMPP server, join an MUC chat room and "lurk" indefinitely, responding with a generic message to anyone that mentions its nickname. It will also greet members as they join the chat room. .. _`multi-user chat`: http://xmpp.org/extensions/xep-0045.html Joining The Room ---------------- As usual, our code will be based on the pattern explained in :ref:`echobot`. To start, we create an ``MUCBot`` class based on :class:`ClientXMPP ` and which accepts parameters for the JID of the MUC room to join, and the nick that the bot will use inside the chat room. We also register an :term:`event handler` for the :term:`session_start` event. .. code-block:: python import slixmpp class MUCBot(slixmpp.ClientXMPP): def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) After initialization, we also need to register the MUC (XEP-0045) plugin so that we can make use of the group chat plugin's methods and events. .. code-block:: python xmpp.register_plugin('xep_0045') Finally, we can make our bot join the chat room once an XMPP session has been established: .. code-block:: python async def start(self, event): await self.get_roster() self.send_presence() self.plugin['xep_0045'].join_muc(self.room, self.nick) Note that as in :ref:`echobot`, we need to include send an initial presence and request the roster. Next, we want to join the group chat, so we call the ``join_muc`` method of the MUC plugin. .. note:: The :attr:`plugin ` attribute is dictionary that maps to instances of plugins that we have previously registered, by their names. Adding Functionality -------------------- Currently, our bot just sits dormantly inside the chat room, but we would like it to respond to two distinct events by issuing a generic message in each case to the chat room. In particular, when a member mentions the bot's nickname inside the chat room, and when a member joins the chat room. Responding to Mentions ~~~~~~~~~~~~~~~~~~~~~~ Whenever a user mentions our bot's nickname in chat, our bot will respond with a generic message resembling *"I heard that, user."* We do this by examining all of the messages sent inside the chat and looking for the ones which contain the nickname string. First, we register an event handler for the :term:`groupchat_message` event inside the bot's ``__init__`` function. .. note:: We do not register a handler for the :term:`message` event in this bot, but if we did, the group chat message would have been sent to both handlers. .. code-block:: python def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) self.add_event_handler("groupchat_message", self.muc_message) Then, we can send our generic message whenever the bot's nickname gets mentioned. .. warning:: Always check that a message is not from yourself, otherwise you will create an infinite loop responding to your own messages. .. code-block:: python def muc_message(self, msg): if msg['mucnick'] != self.nick and self.nick in msg['body']: self.send_message(mto=msg['from'].bare, mbody="I heard that, %s." % msg['mucnick'], mtype='groupchat') Greeting Members ~~~~~~~~~~~~~~~~ Now we want to greet member whenever they join the group chat. To do this we will use the dynamic ``muc::room@server::got_online`` [1]_ event so it's a good idea to register an event handler for it. .. note:: The groupchat_presence event is triggered whenever a presence stanza is received from any chat room, including any presences you send yourself. To limit event handling to a single room, use the events ``muc::room@server::presence``, ``muc::room@server::got_online``, or ``muc::room@server::got_offline``. .. code-block:: python def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick self.add_event_handler("session_start", self.start) self.add_event_handler("groupchat_message", self.muc_message) self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) Now all that's left to do is to greet them: .. code-block:: python def muc_online(self, presence): if presence['muc']['nick'] != self.nick: self.send_message(mto=presence['from'].bare, mbody="Hello, %s %s" % (presence['muc']['role'], presence['muc']['nick']), mtype='groupchat') .. [1] this is similar to the :term:`got_online` event and is sent by the xep_0045 plugin whenever a member joins the referenced MUC chat room. Final Product ------------- .. compound:: The final step is to create a small runner script for initialising our ``MUCBot`` class and adding some basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive at the code below. To experiment with this example, you can use: .. code-block:: sh python muc.py -d -j jid@example.com -r room@muc.example.net -n lurkbot which will prompt for the password, log in, and join the group chat. To test, open your regular IM client and join the same group chat that you sent the bot to. You will see ``lurkbot`` as one of the members in the group chat, and that it greeted you upon entry. Send a message with the string "lurkbot" inside the body text, and you will also see that it responds with our pre-programmed customized message. .. include:: ../../examples/muc.py :literal: slixmpp/docs/getting_started/presence.rst000066400000000000000000000000741477105560000212350ustar00rootroot00000000000000Manage Presence Subscriptions ============================= slixmpp/docs/getting_started/scheduler.rst000066400000000000000000000000761477105560000214110ustar00rootroot00000000000000Send a Message Every 5 Minutes ============================== slixmpp/docs/getting_started/sendlogout.rst000066400000000000000000000070241477105560000216160ustar00rootroot00000000000000Sign in, Send a Message, and Disconnect ======================================= .. note:: If you have any issues working through this quickstart guide join the chat room at `slixmpp@muc.poez.io `_. A common use case for Slixmpp is to send one-off messages from time to time. For example, one use case could be sending out a notice when a shell script finishes a task. We will create our one-shot bot based on the pattern explained in :ref:`echobot`. To start, we create a client class based on :class:`ClientXMPP ` and register a handler for the :term:`session_start` event. We will also accept parameters for the JID that will receive our message, and the string content of the message. .. code-block:: python import slixmpp class SendMsgBot(slixmpp.ClientXMPP): def __init__(self, jid, password, recipient, msg): super().__init__(jid, password) self.recipient = recipient self.msg = msg self.add_event_handler('session_start', self.start) async def start(self, event): self.send_presence() await self.get_roster() Note that as in :ref:`echobot`, we need to include send an initial presence and request the roster. Next, we want to send our message, and to do that we will use :meth:`send_message `. .. code-block:: python async def start(self, event): self.send_presence() await self.get_roster() self.send_message(mto=self.recipient, mbody=self.msg) Finally, we need to disconnect the client using :meth:`disconnect `. Now, sent stanzas are placed in a queue to pass them to the send routine. :meth:`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 ` will not close the connection gracefully. .. code-block:: python async def start(self, event): self.send_presence() await self.get_roster() self.send_message(mto=self.recipient, mbody=self.msg) 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() ` will block and not disconnect. Final Product ------------- .. compound:: The final step is to create a small runner script for initialising our ``SendMsgBot`` class and adding some basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive at the code below. To experiment with this example, you can use: .. code-block:: sh python send_client.py -d -j oneshot@example.com -t someone@example.net -m "This is a message" which will prompt for the password and then log in, send your message, and then disconnect. To test, open your regular IM client with the account you wish to send messages to. When you run the ``send_client.py`` example and instruct it to send your IM client account a message, you should receive the message you gave. If the two JIDs you use also have a mutual presence subscription (they're on each other's buddy lists) then you will also see the ``SendMsgBot`` client come online and then go offline. .. include:: ../../examples/send_client.py :literal: slixmpp/docs/glossary.rst000066400000000000000000000025041477105560000161050ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: stream handler A callback function that accepts stanza objects pulled directly from the XML stream. A stream handler is encapsulated in a object that includes a :class:`Matcher <.MatcherBase>` object which provides additional semantics. event handler A callback function that responds to events raised by :meth:`.XMLStream.event`. stanza object Informally may refer both to classes which extend :class:`.ElementBase` or :class:`.StanzaBase`, and to objects of such classes. A stanza object is a wrapper for an XML object which exposes :class:`dict` like interfaces which may be assigned to, read from, or deleted. stanza plugin A :term:`stanza object` which has been registered as a potential child of another stanza object. The plugin stanza may accessed through the parent stanza using the plugin's ``plugin_attrib`` as an interface. substanza See :term:`stanza plugin` interfaces A set of keys defined on a :term:`stanza plugin`. stanza An XML payload sent over the XML stream, which is the root of XMPP. A stanza is either ````, ```` or ````. Other elements are called nonzas. slixmpp/docs/howto/000077500000000000000000000000001477105560000146475ustar00rootroot00000000000000slixmpp/docs/howto/create_plugin.rst000066400000000000000000000615021477105560000202260ustar00rootroot00000000000000.. _create-plugin: Creating a Slixmpp Plugin =========================== One of the goals of Slixmpp is to provide support for every draft or final XMPP extension (`XEP `_). To do this, Slixmpp has a plugin mechanism for adding the functionalities required by each XEP. But even though plugins were made to quickly implement and prototype the official XMPP extensions, there is no reason you can't create your own plugin to implement your own custom XMPP-based protocol. This guide will help walk you through the steps to implement a rudimentary version of `XEP-0077 In-band Registration `_. In-band registration was implemented in example 14-6 (page 223) of `XMPP: The Definitive Guide `_ because there was no Slixmpp plugin for XEP-0077 at the time of writing. We will partially fix that issue here by turning the example implementation from *XMPP: The Definitive Guide* into a plugin. Again, note that this will not a complete implementation, and a different, more robust, official plugin for XEP-0077 may be added to Slixmpp in the future. .. note:: The example plugin created in this guide is for the server side of the registration process only. It will **NOT** be able to register new accounts on an XMPP server. First Steps ----------- Every plugin inherits from the class :mod:`BasePlugin `_. To do that, we tell the ``xep_0030`` plugin to add the ``"jabber:iq:register"`` feature. We put this call in a method named ``post_init`` which will be called once the plugin has been loaded; by doing so we advertise that we can do registrations only after we finish activating the plugin. The ``post_init`` method needs to call ``BasePlugin.post_init(self)`` which will mark that ``post_init`` has been called for the plugin. Once the Slixmpp object begins processing, ``post_init`` will be called on any plugins that have not already run ``post_init``. This allows you to register plugins and their dependencies without needing to worry about the order in which you do so. **Note:** by adding this call we have introduced a dependency on the XEP-0030 plugin. Be sure to register ``'xep_0030'`` as well as ``'xep_0077'``. Slixmpp does not automatically load plugin dependencies for you. .. code-block:: python def post_init(self): BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature("jabber:iq:register") Creating Custom Stanza Objects ------------------------------ Now, the IQ stanzas needed to implement our version of XEP-0077 are not very complex, and we could just interact with the XML objects directly just like in the *XMPP: The Definitive Guide* example. However, creating custom stanza objects is good practice. We will create a new ``Registration`` stanza. Following the *XMPP: The Definitive Guide* example, we will add support for a username and password field. We also need two flags: ``registered`` and ``remove``. The ``registered`` flag is sent when an already registered user attempts to register, along with their registration data. The ``remove`` flag is a request to unregister a user's account. Adding additional `fields specified in XEP-0077 `_ will not be difficult and is left as an exercise for the reader. Our ``Registration`` class needs to start with a few descriptions of its behaviour: * ``namespace`` The namespace our stanza object lives in. In this case, ``"jabber:iq:register"``. * ``name`` The name of the root XML element. In this case, the ``query`` element. * ``plugin_attrib`` The name to access this type of stanza. In particular, given a registration stanza, the ``Registration`` object can be found using: ``iq_object['register']``. * ``interfaces`` A list of dictionary-like keys that can be used with the stanza object. When using ``"key"``, if there exists a method of the form ``getKey``, ``setKey``, or``delKey`` (depending on context) then the result of calling that method will be returned. Otherwise, the value of the attribute ``key`` of the main stanza element is returned if one exists. **Note:** The accessor methods currently use title case, and not camel case. Thus if you need to access an item named ``"methodName"`` you will need to use ``getMethodname``. This naming convention might change to full camel case in a future version of Slixmpp. * ``sub_interfaces`` A subset of ``interfaces``, but these keys map to the text of any subelements that are direct children of the main stanza element. Thus, referencing ``iq_object['register']['username']`` will either execute ``getUsername`` or return the value in the ``username`` element of the query. If you need to access an element, say ``elem``, that is not a direct child of the main stanza element, you will need to add ``getElem``, ``setElem``, and ``delElem``. See the note above about naming conventions. .. code-block:: python from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin from slixmpp import Iq class Registration(ElementBase): namespace = 'jabber:iq:register' name = 'query' plugin_attrib = 'register' interfaces = {'username', 'password', 'registered', 'remove'} sub_interfaces = interfaces def getRegistered(self): present = self.xml.find('{%s}registered' % self.namespace) return present is not None def getRemove(self): present = self.xml.find('{%s}remove' % self.namespace) return present is not None def setRegistered(self, registered): if registered: self.addField('registered') else: del self['registered'] def setRemove(self, remove): if remove: self.addField('remove') else: del self['remove'] def addField(self, name): itemXML = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(itemXML) Setting a ``sub_interface`` attribute to ``""`` will remove that subelement. Since we want to include empty registration fields in our form, we need the ``addField`` method to add the empty elements. Since the ``registered`` and ``remove`` elements are just flags, we need to add custom logic to enforce the binary behavior. Extracting Stanzas from the XML Stream -------------------------------------- Now that we have a custom stanza object, we need to be able to detect when we receive one. To do this, we register a stream handler that will pattern match stanzas off of the XML stream against our stanza object's element name and namespace. To do so, we need to create a ``Callback`` object which contains an XML fragment that can identify our stanza type. We can add this handler registration to our ``plugin_init`` method. Also, we need to associate our ``Registration`` class with IQ stanzas; that requires the use of the ``register_stanza_plugin`` function (in ``slixmpp.xmlstream.stanzabase``) which takes the class of a parent stanza type followed by the substanza type. In our case, the parent stanza is an IQ stanza, and the substanza is our registration query. The ``__handleRegistration`` method referenced in the callback will be our handler function to process registration requests. .. code-block:: python def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) register_stanza_plugin(Iq, Registration) Handling Incoming Stanzas and Triggering Events ----------------------------------------------- There are six situations that we need to handle to finish our implementation of XEP-0077. **Registration Form Request from a New User:** .. code-block:: xml **Registration Form Request from an Existing User:** .. code-block:: xml Foo hunter2 **Unregister Account:** .. code-block:: xml **Incomplete Registration:** .. code-block:: xml Foo **Conflicting Registrations:** .. code-block:: xml Foo hunter2 **Successful Registration:** .. code-block:: xml Cases 1 and 2: Registration Requests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Responding to registration requests depends on if the requesting user already has an account. If there is an account, the response should include the ``registered`` flag and the user's current registration information. Otherwise, we just send the fields for our registration form. We will handle both cases by creating a ``sendRegistrationForm`` method that will create either an empty of full form depending on if we provide it with user data. Since we need to know which form fields to include (especially if we add support for the other fields specified in XEP-0077), we will also create a method ``setForm`` which will take the names of the fields we wish to include. .. code-block:: python def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.form_fields = ('username', 'password') ... remainder of plugin_init ... def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) def setForm(self, *fields): self.form_fields = fields def sendRegistrationForm(self, iq, userData=None): reg = iq['register'] if userData is None: userData = {} else: reg['registered'] = True for field in self.form_fields: data = userData.get(field, '') if data: # Add field with existing data reg[field] = data else: # Add a blank field reg.addField(field) iq.reply().set_payload(reg.xml) iq.send() Note how we are able to access our ``Registration`` stanza object with ``iq['register']``. A User Backend ++++++++++++++ You might have noticed the reference to ``self.backend``, which is an object that abstracts away storing and retrieving user information. Since it is not much more than a dictionary, we will leave the implementation details to the final, full source code example. Case 3: Unregister an Account ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The next simplest case to consider is responding to a request to remove an account. If we receive a ``remove`` flag, we instruct the backend to remove the user's account. Since your application may need to know about when users are registered or unregistered, we trigger an event using ``self.xmpp.event('unregister_user', iq)``. See the component examples below for how to respond to that event. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': # Remove an account if iq['register']['remove']: self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return Case 4: Incomplete Registration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For the next case we need to check the user's registration to ensure it has all of the fields we wanted. The simple option that we will use is to loop over the field names and check each one; however, this means that all fields we send to the user are required. Adding optional fields is left to the reader. Since we have received an incomplete form, we need to send an error message back to the user. We have to send a few different types of errors, so we will also create a ``_sendError`` method that will add the appropriate ``error`` element to the IQ reply. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable' "Please fill in all fields.") return ... def _sendError(self, iq, code, error_type, name, text=''): iq.reply().set_payload(iq['register'].xml) iq.error() iq['error']['code'] = code iq['error']['type'] = error_type iq['error']['condition'] = name iq['error']['text'] = text iq.send() Cases 5 and 6: Conflicting and Successful Registration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We are down to the final decision on if we have a successful registration. We send the user's data to the backend with the ``self.backend.register`` method. If it returns ``True``, then registration has been successful. Otherwise, there has been a conflict with usernames and registration has failed. Like with unregistering an account, we trigger an event indicating that a user has been registered by using ``self.xmpp.event('registered_user', iq)``. See the component examples below for how to respond to this event. .. code-block:: python def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable', "Please fill in all fields.") return if self.backend.register(iq['from'].bare, iq['register']): # Successful registration self.xmpp.event('registered_user', iq) iq.reply().set_payload(iq['register'].xml) iq.send() else: # Conflicting registration self._sendError(iq, '409', 'cancel', 'conflict', "That username is already taken.") Example Component Using the XEP-0077 Plugin ------------------------------------------- Alright, the moment we've been working towards - actually using our plugin to simplify our other applications. Here is a basic component that simply manages user registrations and sends the user a welcoming message when they register, and a farewell message when they delete their account. Note that we have to register the ``'xep_0030'`` plugin first, and that we specified the form fields we wish to use with ``self.xmpp.plugin['xep_0077'].setForm('username', 'password')``. .. code-block:: python import slixmpp.componentxmpp class Example(slixmpp.componentxmpp.ComponentXMPP): def __init__(self, jid, password): slixmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 8888) self.register_plugin('xep_0030') self.register_plugin('xep_0077') self.plugin['xep_0077'].setForm('username', 'password') self.add_event_handler("registered_user", self.reg) self.add_event_handler("unregistered_user", self.unreg) def reg(self, iq): msg = "Welcome! %s" % iq['register']['username'] self.send_message(iq['from'], msg, mfrom=self.fulljid) def unreg(self, iq): msg = "Bye! %s" % iq['register']['username'] self.send_message(iq['from'], msg, mfrom=self.fulljid) **Congratulations!** We now have a basic, functioning implementation of XEP-0077. Complete Source Code for XEP-0077 Plugin ---------------------------------------- Here is a copy of a more complete implementation of the plugin we created, but with some additional registration fields implemented. .. code-block:: python """ Creating a Slixmpp Plugin This is a minimal implementation of XEP-0077 to serve as a tutorial for creating Slixmpp plugins. """ from slixmpp.plugins.base import BasePlugin from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.xpath import MatchXPath from slixmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin from slixmpp import Iq import copy class Registration(ElementBase): namespace = 'jabber:iq:register' name = 'query' plugin_attrib = 'register' interfaces = {'username', 'password', 'email', 'nick', 'name', 'first', 'last', 'address', 'city', 'state', 'zip', 'phone', 'url', 'date', 'misc', 'text', 'key', 'registered', 'remove', 'instructions'} sub_interfaces = interfaces def getRegistered(self): present = self.xml.find('{%s}registered' % self.namespace) return present is not None def getRemove(self): present = self.xml.find('{%s}remove' % self.namespace) return present is not None def setRegistered(self, registered): if registered: self.addField('registered') else: del self['registered'] def setRemove(self, remove): if remove: self.addField('remove') else: del self['remove'] def addField(self, name): itemXML = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(itemXML) class UserStore(object): def __init__(self): self.users = {} def __getitem__(self, jid): return self.users.get(jid, None) def register(self, jid, registration): username = registration['username'] def filter_usernames(user): return user != jid and self.users[user]['username'] == username conflicts = filter(filter_usernames, self.users.keys()) if conflicts: return False self.users[jid] = registration return True def unregister(self, jid): del self.users[jid] class xep_0077(BasePlugin): """ XEP-0077 In-Band Registration """ def plugin_init(self): self.description = "In-Band Registration" self.xep = "0077" self.form_fields = ('username', 'password') self.form_instructions = "" self.backend = UserStore() self.xmpp.register_handler( Callback('In-Band Registration', MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns), self.__handleRegistration)) register_stanza_plugin(Iq, Registration) def post_init(self): BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature("jabber:iq:register") def __handleRegistration(self, iq): if iq['type'] == 'get': # Registration form requested userData = self.backend[iq['from'].bare] self.sendRegistrationForm(iq, userData) elif iq['type'] == 'set': if iq['register']['remove']: # Remove an account self.backend.unregister(iq['from'].bare) self.xmpp.event('unregistered_user', iq) iq.reply().send() return for field in self.form_fields: if not iq['register'][field]: # Incomplete Registration self._sendError(iq, '406', 'modify', 'not-acceptable', "Please fill in all fields.") return if self.backend.register(iq['from'].bare, iq['register']): # Successful registration self.xmpp.event('registered_user', iq) reply = iq.reply() reply.set_payload(iq['register'].xml) reply.send() else: # Conflicting registration self._sendError(iq, '409', 'cancel', 'conflict', "That username is already taken.") def setForm(self, *fields): self.form_fields = fields def setInstructions(self, instructions): self.form_instructions = instructions def sendRegistrationForm(self, iq, userData=None): reg = iq['register'] if userData is None: userData = {} else: reg['registered'] = True if self.form_instructions: reg['instructions'] = self.form_instructions for field in self.form_fields: data = userData.get(field, '') if data: # Add field with existing data reg[field] = data else: # Add a blank field reg.addField(field) reply = iq.reply() reply.set_payload(reg.xml) reply.send() def _sendError(self, iq, code, error_type, name, text=''): reply = iq.reply() reply.set_payload(iq['register'].xml) reply.error() reply['error']['code'] = code reply['error']['type'] = error_type reply['error']['condition'] = name reply['error']['text'] = text reply.send() slixmpp/docs/howto/features.rst000066400000000000000000000000661477105560000172210ustar00rootroot00000000000000How to Use Stream Features ========================== slixmpp/docs/howto/guide_xep_0030.rst000066400000000000000000000214701477105560000200200ustar00rootroot00000000000000XEP-0030: Working with Service Discovery ======================================== XMPP networks can be composed of many individual clients, components, and servers. Determining the JIDs for these entities and the various features they may support is the role of `XEP-0030, Service Discovery `_, or "disco" for short. Every XMPP entity may possess what are called nodes. A node is just a name for some aspect of an XMPP entity. For example, if an XMPP entity provides `Ad-Hoc Commands `_, then it will have a node named ``http://jabber.org/protocol/commands`` which will contain information about the commands provided. Other agents using these ad-hoc commands will interact with the information provided by this node. Note that the node name is just an identifier; there is no inherent meaning. Working with service discovery is about creating and querying these nodes. According to XEP-0030, a node may contain three types of information: identities, features, and items. (Further, extensible, information types are defined in `XEP-0128 `_, but they are not yet implemented by Slixmpp.) Slixmpp provides methods to configure each of these node attributes. Configuring Service Discovery ----------------------------- The design focus for the XEP-0030 plug-in is handling info and items requests in a dynamic fashion, allowing for complex policy decisions of who may receive information and how much, or use alternate backend storage mechanisms for all of the disco data. To do this, each action that the XEP-0030 plug-in performs is handed off to what is called a "node handler," which is just a callback function. These handlers are arranged in a hierarchy that allows for a single handler to manage an entire domain of JIDs (say for a component), while allowing other handler functions to override that global behaviour for certain JIDs, or even further limited to only certain JID and node combinations. The Dynamic Handler Hierarchy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * ``global``: (JID is None, node is None) Handlers assigned at this level for an action (such as ``add_feature``) provide a global default behaviour when the action is performed. * ``jid``: (JID assigned, node is None) At this level, handlers provide a default behaviour for actions affecting any node owned by the JID in question. This level is most useful for component connections; there is effectively no difference between this and the global level when using a client connection. * ``node``: (JID assigned, node assigned) A handler for this level is responsible for carrying out an action for only one node, and is the most specific handler type available. These types of handlers will be most useful for "special" nodes that require special processing different than others provided by the JID, such as using access control lists, or consolidating data from other nodes. Default Static Handlers ~~~~~~~~~~~~~~~~~~~~~~~ The XEP-0030 plug-in provides a default set of handlers that work using in-memory disco stanzas. Each handler simply performs the appropriate lookup or storage operation using these stanzas without doing any complex operations such as checking an ACL, etc. You may find it necessary at some point to revert a particular node or JID to using the default, static handlers. To do so, use the method ``restore_defaults()``. You may also elect to only convert a given set of actions instead. Creating a Node Handler ~~~~~~~~~~~~~~~~~~~~~~~ Every node handler receives three arguments: the JID, the node, and a data parameter that will contain the relevant information for carrying out the handler's action, typically a dictionary. The JID will always have a value, defaulting to ``xmpp.boundjid.full`` for components or ``xmpp.boundjid.bare`` for clients. The node value may be None or a string. Only handlers for the actions ``get_info`` and ``get_items`` need to have return values. For these actions, DiscoInfo or DiscoItems stanzas are exepected as output. It is also acceptable for handlers for these actions to generate an XMPPError exception when necessary. Example Node Handler: +++++++++++++++++++++ Here is one of the built-in default handlers as an example: .. code-block:: python def add_identity(self, jid, node, data): """ Add a new identity to the JID/node combination. The data parameter may provide: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ self.add_node(jid, node) self.nodes[(jid, node)]['info'].add_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) Adding Identities, Features, and Items -------------------------------------- In order to maintain some backwards compatibility, the methods ``add_identity``, ``add_feature``, and ``add_item`` do not follow the method signature pattern of the other API methods (i.e. jid, node, then other options), but rather retain the parameter orders from previous plug-in versions. Adding an Identity ~~~~~~~~~~~~~~~~~~ Adding an identity may be done using either the older positional notation, or with keyword parameters. The example below uses the keyword arguments, but in the same order as expected using positional arguments. .. code-block:: python xmpp['xep_0030'].add_identity(category='client', itype='bot', name='Slixmpp', node='foo', jid=xmpp.boundjid.full, lang='no') The JID and node values determine which handler will be used to perform the ``add_identity`` action. The ``lang`` parameter allows for adding localized versions of identities using the ``xml:lang`` attribute. Adding a Feature ~~~~~~~~~~~~~~~~ The position ordering for ``add_feature()`` is to include the feature, then specify the node and then the JID. The JID and node values determine which handler will be used to perform the ``add_feature`` action. .. code-block:: python xmpp['xep_0030'].add_feature(feature='jabber:x:data', node='foo', jid=xmpp.boundjid.full) Adding an Item ~~~~~~~~~~~~~~ The parameters to ``add_item()`` are potentially confusing due to the fact that adding an item requires two JID and node combinations: the JID and node of the item itself, and the JID and node that will own the item. .. code-block:: python xmpp['xep_0030'].add_item(jid='myitemjid@example.com', name='An Item!', node='owner_node', subnode='item_node', ijid=xmpp.boundjid.full) .. note:: In this case, the owning JID and node are provided with the parameters ``ijid`` and ``node``. Performing Disco Queries ------------------------ The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs and their nodes for disco information. Since these methods are wrappers for sending Iq stanzas, they also accept all of the parameters of the ``Iq.send()`` method. The ``get_items()`` method may also accept the boolean parameter ``iterator``, which when set to ``True`` will return an iterator object using the `XEP-0059 `_ plug-in. .. code-block:: python info = await self['xep_0030'].get_info(jid='foo@example.com', node='bar', ifrom='baz@mycomponent.example.com', timeout=30) items = await self['xep_0030'].get_items(jid='foo@example.com', node='bar', iterator=True) For more examples on how to use basic disco queries, check the ``disco_browser.py`` example in the ``examples`` directory. Local Queries ~~~~~~~~~~~~~ In some cases, it may be necessary to query the contents of a node owned by the client itself, or one of a component's many JIDs. The same method is used as for normal queries, with two differences. First, the parameter ``local=True`` must be used. Second, the return value will be a DiscoInfo or DiscoItems stanza, not a full Iq stanza. .. code-block:: python info = await self['xep_0030'].get_info(node='foo', local=True) items = await self['xep_0030'].get_items(jid='somejid@mycomponent.example.com', node='bar', local=True) slixmpp/docs/howto/handlersmatchers.rst000066400000000000000000000001441477105560000207270ustar00rootroot00000000000000.. _using-handlers-matchers: Using Stream Handlers and Matchers ================================== slixmpp/docs/howto/index.rst000066400000000000000000000003631477105560000165120ustar00rootroot00000000000000Tutorials, FAQs, and How To Guides ---------------------------------- .. toctree:: :maxdepth: 2 stanzas create_plugin internal_api features sasl remove_process handlersmatchers guide_xep_0030 xmpp_tdg slixmpp/docs/howto/internal_api.rst000066400000000000000000000056561477105560000200620ustar00rootroot00000000000000.. _api-simple-tuto: Flexible internal API usage =========================== The :ref:`internal-api` in slixmpp is used to override behavior or simply to override the default, in-memory storage backend with something persistent. We will use the XEP-0231 (Bits of Binary) plugin as an example here to show very basic functionality. Its API reference is in the plugin documentation: :ref:`api-0231`. Let us assume we want to keep each bit of binary in a file named with its content-id, with all metadata. First, we have to load the plugin: .. code-block:: python from slixmpp import ClientXMPP xmpp = ClientXMPP(...) xmpp.register_plugin('xep_0231') This enables the default, in-memory storage. We have 3 methods to override to provide similar functionality and keep things coherent. Here is a class implementing very basic file storage for BoB: .. code-block:: python from slixmpp.plugins.xep_0231 import BitsOfBinary from os import makedirs, remove from os.path import join, exists import base64 import json class BobLoader: def __init__(self, directory): makedirs(directory, exist_ok=True) self.dir = directory def set_bob(self, jid=None, node=None, ifrom=None, args=None): payload = { 'data': base64.b64encode(args['data']).decode(), 'type': args['type'], 'cid': args['cid'], 'max_age': args['max_age'] } with open(join(self.dir, args['cid']), 'w') as fd: fd.write(json.dumps(payload)) def get_bob(self, jid=None, node=None, ifrom=None, args=None): with open(join(self.dir, args), 'r') as fd: payload = json.loads(fd.read()) bob = BitsOfBinary() bob['data'] = base64.b64decode(payload['data']) bob['type'] = payload['type'] bob['max_age'] = payload['max_age'] bob['cid'] = payload['cid'] return bob def del_bob(self, jid=None, node=None, ifrom=None, args=None): path = join(self.dir, args) if exists(path): remove(path) Now we need to replace the default handler with ours: .. code-block:: python bobhandler = BobLoader('/tmp/bobcache') xmpp.plugin['xep_0231'].api.register(bobhandler.set_bob, 'set_bob') xmpp.plugin['xep_0231'].api.register(bobhandler.get_bob, 'get_bob') xmpp.plugin['xep_0231'].api.register(bobhandler.del_bob, 'del_bob') And that’s it, the BoB storage is now made of JSON files living in a directory (``/tmp/bobcache`` here). To check that everything works, you can do the following: .. code-block:: python cid = await xmpp.plugin['xep_0231'].set_bob(b'coucou', 'text/plain') # A new bob file should appear content = await xmpp.plugin['xep_0231'].get_bob(cid=cid) assert content['bob']['data'] == b'coucou' A file should have been created in that directory. slixmpp/docs/howto/make_plugin_extension_for_message_and_iq.pl.rst000066400000000000000000002642711477105560000263030ustar00rootroot00000000000000Jak 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 asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass .. code-block:: python #File: $WORKDIR/example/responder.py import logging from argparse import ArgumentParser from getpass import getpass import asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) 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: 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. . 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 , 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) # Info_inside_tag #<<<<<<<<<<<< #>>>>>>>>>>>> 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. . 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. . 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 Info_inside_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) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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. . 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 asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass .. code-block:: python #File: $WORKDIR/example/sender.py import logging from argparse import ArgumentParser from getpass import getpass import time import asyncio 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) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) 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: itemXML.attrib.update(attributes) #~ Przypisujemy zdefiniowane atrybuty, na przykład: itemXML.text = text #~ Dodajemy text wewnątrz tego tagu: our_text 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 . 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: itemXML.attrib.update(attributes) #~ There we add some fields inside tag, for example: itemXML.text = text #~ Fill field inside tag, for example: our_text 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 asyncio 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) # Info_inside_tag self.send_example_iq_with_inner_tag(self.to) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass ~ .. code-block:: python #File: $WORKDIR/example/responder.py import logging from argparse import ArgumentParser from getpass import getpass import time import asyncio 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) # Info_inside_tag self.send_example_iq_with_inner_tag(self.to) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass ~ .. code-block:: python #File: $WORKDIR/test_example_tag.xml .. code-block:: xml Info_inside_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. slixmpp/docs/howto/make_plugin_extension_for_message_and_iq.rst000066400000000000000000002546001477105560000256640ustar00rootroot00000000000000How 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 asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass .. code-block:: python #File: $WORKDIR/example/responder.py import logging from argparse import ArgumentParser from getpass import getpass import asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) 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 = "" ##~ The namespace of the object, like . 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 . 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 , 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) # Info_inside_tag #<<<<<<<<<<<< #>>>>>>>>>>>> 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 . 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 . 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 Info_inside_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) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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 . 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 asyncio 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass .. code-block:: python #File: $WORKDIR/example/sender.py import logging from argparse import ArgumentParser from getpass import getpass import time import asyncio 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) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) 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: itemXML.attrib.update(attributes) #~ Here we add some fields inside tag, for example: itemXML.text = text #~ Fill field inside tag, for example: our_text 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 . 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: itemXML.attrib.update(attributes) #~ There we add some fields inside tag, for example: itemXML.text = text #~ Fill field inside tag, for example: our_text 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 asyncio 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) # Info_inside_tag self.send_example_iq_with_inner_tag(self.to) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass ~ .. code-block:: python #File: $WORKDIR/example/responder.py import logging from argparse import ArgumentParser from getpass import getpass import time import asyncio 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) # Info_inside_tag self.send_example_iq_with_inner_tag(self.to) # Info_inside_tag self.send_example_message(self.to) # Info_inside_tag_message self.send_example_iq_tag_from_file(self.to, self.path) # Info_inside_tag string = 'Info_inside_tag' et = ET.fromstring(string) self.send_example_iq_tag_from_element_tree(self.to, et) # Info_inside_tag self.send_example_iq_to_get_error(self.to) # # OUR ERROR Without boolean value returns error. # OFFLINE ERROR User session not found self.send_example_iq_tag_from_string(self.to, string) # Info_inside_tag 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: asyncio.get_event_loop().run_forever() except KeyboardInterrupt: try: xmpp.disconnect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) except: pass ~ .. code-block:: python #File: $WORKDIR/test_example_tag.xml .. code-block:: xml Info_inside_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. slixmpp/docs/howto/remove_process.rst000066400000000000000000000032171477105560000204370ustar00rootroot00000000000000.. _remove-process: How to remove xmpp.process() ============================ Starting from slixmpp 1.8.0, running ``process()`` on an XMLStream/ClientXMPP/ComponentXMPP instance is deprecated, and starting from 1.9.0, it will be removed. Why --- This has been the usual way of running an application using SleekXMPP/slixmpp for ages, but it has come at a price: people do not understand how they should run their application without it, or how to integrate their slixmpp code with the rest of their asyncio application. In essence, ``process()`` is only a very thin wrapper around asyncio loop functions: .. code-block:: python if timeout is None: if forever: self.loop.run_forever() else: self.loop.run_until_complete(self.disconnected) else: tasks: List[Future] = [asyncio.sleep(timeout)] if not forever: tasks.append(self.disconnected) self.loop.run_until_complete(asyncio.wait(tasks)) How --- Hence it can be replaced according to what you want your application to do: - To run forever, ``loop.run_forever()`` will work just fine - To run until disconnected, ``loop.run_until_complete(xmpp.disconnected)`` will be enough (XMLStream.disconnected is an future which result is set when the stream gets disconnected. - To run for a scheduled time (and still abort when disconnected): .. code-block:: python tasks = [asyncio.sleep(timeout)] tasks.append(xmpp.disconnected) loop.run_until_complete(asyncio.wait(tasks)) There is no magic at play here and anything is possible if a more flexible execution scheme is expected. slixmpp/docs/howto/sasl.rst000066400000000000000000000000741477105560000163440ustar00rootroot00000000000000How SASL Authentication Works ============================= slixmpp/docs/howto/stanzas.rst000066400000000000000000000264401477105560000170720ustar00rootroot00000000000000.. _work-with-stanzas: How to Work with Stanza Objects =============================== Slixmpp provides a large variety of facilities for abstracting the underlying XML payloads of XMPP. Most of the visible user interface comes in a dict-like interface provided in a specific ``__getitem__`` implementation for :class:`~slixmpp.xmlstream.ElementBase` objects. As a very high-level example, here is how to create a stanza with an XEP-0191 payload, assuming the :class:`xep_0191 ` plugin is loaded: .. code-block:: python from slixmpp.stanza import Iq iq = Iq() iq['to'] = 'toto@example.com' iq['type'] = 'set' iq['block']['items'] = {'a@example.com', 'b@example.com'} Printing the resulting :class:`~slixmpp.stanaz.Iq` object gives us the following XML (reformatted for readability): .. code-block:: xml Realistically, users of the Slixmpp library should make use of the shorthand functions available in their :class:`~.ClientXMPP` or :class:`~.ComponentXMPP` objects to create :class:`~.Iq`, :class:`~.Message` or :class:`~.Presence` objects that are bound to a stream, and which have a generated unique identifier. The most relevant functions are: .. autofunction:: slixmpp.BaseXMPP.make_iq_get .. autofunction:: slixmpp.BaseXMPP.make_iq_set .. autofunction:: slixmpp.BaseXMPP.make_message .. autofunction:: slixmpp.BaseXMPP.make_presence The previous example then becomes: .. code-block:: python iq = xmpp.make_iq_get(ito='toto@example.com') iq['block']['items'] = {'a@example.com', 'b@example.com'} .. note:: xml:lang is handled by piping the lang name after the attribute. For example ``message['body|fr']`` will return the ```` attribute with ``xml:lang="fr``. The next sections will try to explain as clearly as possible how the magic operates. .. _create-stanza-interfaces: Defining Stanza Interfaces -------------------------- The stanza interface is very rich and let developers have full control over the API they want to have to manipulate stanzas. The entire interface is defined as class attributes that are redefined when subclassing :class:`~.ElementBase` when `creating a stanza plugin `_. The main attributes defining a stanza interface: - plugin_attrib_: ``str``, the name of this element on the parent - plugin_multi_attrib_: ``str``, the name of the iterable for this element on the parent - interfaces_: ``set``, all known interfaces for this element - sub_interfaces_: ``set`` (subset of ``interfaces``), for sub-elements with only text nodes - bool_interfaces_: ``set`` (subset of ``interfaces``), for empty-sub-elements - overrides_: ``list`` (subset of ``interfaces``), for ``interfaces`` to ovverride on the parent - is_extension_: ``bool``, if the element is only an extension of the parent stanza .. _plugin_attrib: plugin_attrib ~~~~~~~~~~~~~ The ``plugin_attrib`` string is the defining element of any stanza plugin, as it the name through which the element is accessed (except for ``overrides`` and ``is_extension``). The extension is then registered through the help of :func:`~.register_stanza_plugin` which will attach the plugin to its parent. .. code-block:: python from slixmpp import ElementBase, Iq class Payload(ElementBase): name = 'apayload' plugin_attrib = 'mypayload' namespace = 'x-toto' register_stanza_plugin(Iq, Payload) iq = Iq() iq.enable('mypayload') # Similar to iq['mypayload'] The :class:`~.Iq` element created now contains our custom ```` element. .. code-block:: xml .. _plugin_multi_attrib: plugin_multi_attrib ~~~~~~~~~~~~~~~~~~~ The :func:`~.register_stanza_plugin` function has an ``iterable`` parameter, which defaults to ``False``. When set to ``True``, it means that iterating over the element is possible. .. code-block:: python class Parent(ElementBase): pass # does not matter class Sub(ElementBase): name = 'sub' plugin_attrib = 'sub' class Sub2(ElementBase): name = 'sub2' plugin_attrib = 'sub2' register_stanza_plugin(Parent, Sub, iterable=True) register_stanza_plugin(Parent, Sub2, iterable=True) parent = Parent() parent.append(Sub()) parent.append(Sub2()) parent.append(Sub2()) parent.append(Sub()) for element in parent: do_something # A mix of Sub and Sub2 elements In this situation, iterating over ``parent`` will yield each of the appended elements, one after the other. Sometimes you only want one specific type of sub-element, which is the use of the ``plugin_multi_attrib`` string interface. This name will be mapped on the parent, just like ``plugin_attrib``, but will return a list of all elements of the same type only. Re-using our previous example: .. code-block:: python class Parent(ElementBase): pass # does not matter class Sub(ElementBase): name = 'sub' plugin_attrib = 'sub' plugin_multi_attrib = 'subs' class Sub2(ElementBase): name = 'sub2' plugin_attrib = 'sub2' plugin_multi_attrib = 'subs2' register_stanza_plugin(Parent, Sub, iterable=True) register_stanza_plugin(Parent, Sub2, iterable=True) parent = Parent() parent.append(Sub()) parent.append(Sub2()) parent.append(Sub2()) parent.append(Sub()) for sub in parent['subs']: do_something # ony Sub objects here for sub2 in parent['subs2']: do_something # ony Sub2 objects here .. _interfaces: interfaces ~~~~~~~~~~ The ``interfaces`` set **must** contain all the known ways to interact with this element. It does not include plugins (registered to the element through :func:`~.register_stanza_plugin`), which are dynamic. By default, a name present in ``interfaces`` will be mapped to an attribute of the element with the same name. .. code-block:: python class Example(Element): name = 'example' interfaces = {'toto'} example = Example() example['toto'] = 'titi' In this case, ``example`` contains ````. For empty and text_only sub-elements, there are sub_interfaces_ and bool_interfaces_ (the keys **must** still be in ``interfaces``. You can however define any getter, setter, and delete custom method for any of those interfaces. Keep in mind that if one of the three is not custom, Slixmpp will use the default one, so you have to make sure that either you redefine all get/set/del custom methods, or that your custom methods are compatible with the default ones. In the following example, we want the ``toto`` attribute to be an integer. .. code-block:: python class Example(Element): interfaces = {'toto', 'titi', 'tata'} def get_toto(self) -> Optional[int]: try: return int(self.xml.attrib.get('toto', '')) except ValueError: return None def set_toto(self, value: int): int(value) # make sure the value is an int self.xml.attrib['toto'] = str(value) example = Example() example['tata'] = "Test" # works example['toto'] = 1 # works print(type(example['toto'])) # the value is an int example['toto'] = "Test 2" # ValueError One important thing to keep in mind is that the ``get_`` methods must be resilient (when having a default value makes sense) because they are called on objects received from the network. .. _sub_interfaces: sub_interfaces ~~~~~~~~~~~~~~ The ``bool_interfaces`` set allows mapping an interface to the text node of sub-element of the current payload, with the same namespace Here is a simple example: .. code-block:: python class FirstLevel(ElementBase): name = 'first' namespace = 'ns' interfaces = {'second'} sub_interfaces = {'second'} parent = FirstLevel() parent['second'] = 'Content of second node' Which will produces the following: .. code-block:: xml Content of second node We can see that ``sub_interfaces`` allows to quickly create a sub-element and manipulate its text node without requiring a custom element, getter or setter. .. _bool_interfaces: bool_interfaces ~~~~~~~~~~~~~~~ The ``bool_interfaces`` set allows mapping an interface to a direct sub-element of the current payload, with the same namespace. Here is a simple example: .. code-block:: python class FirstLevel(ElementBase): name = 'first' namespace = 'ns' interfaces = {'second'} bool_interfaces = {'second'} parent = FirstLevel() parent['second'] = True Which will produces the following: .. code-block:: xml We can see that ``bool_interfaces`` allows to quickly create sub-elements with no content, without the need to create a custom class or getter/setter. .. _overrides: overrides ~~~~~~~~~ List of ``interfaces`` on the present element that should override the parent ``interfaces`` with the same name. .. code-block:: python class Parent(ElementBase): name = 'parent' interfaces = {'toto', 'titi'} class Sub(ElementBase): name = 'sub' plugin_attrib = name interfaces = {'toto', 'titi'} overrides = ['toto'] register_stanza_plugin(Parent, Sub) parent = Parent() parent['toto'] = 'test' # equivalent to parent['sub']['toto'] = "test" .. _is_extension: is_extension ~~~~~~~~~~~~ Stanza extensions are a specific kind of stanza plugin which have the ``is_extension`` class attribute set to ``True``. The following code will directly plug the extension into the :class:`~.Message` element, allowing direct access to the interface: .. code-block:: python class MyCustomExtension(ElementBase): is_extension = True name = 'mycustom' namespace = 'custom-ns' plugin_attrib = 'mycustom' interfaces = {'mycustom'} register_stanza_plugin(Message, MyCustomExtension) With this extension, we can do the folliowing: .. code-block:: python message = Message() message['mycustom'] = 'toto' Without the extension, obtaining the same results would be: .. code-block:: python message = Message() message['mycustom']['mycustom'] = 'toto' The extension is therefore named extension because it extends the parent element transparently. .. _create-stanza-plugins: Creating Stanza Plugins ----------------------- A stanza plugin is a class that inherits from :class:`~.ElementBase`, and **must** contain at least the following attributes: - name: XML element name (e.g. ``toto`` if the element is ```` - namespace: The XML namespace of the element. - plugin_attrib_: ``str``, the name of this element on the parent - interfaces_: ``set``, all known interfaces for this element It is then registered through :func:`~.register_stanza_plugin` on the parent element. .. note:: :func:`~.register_stanza_plugin` should NOT be called at the module level, because it executes code, and executing code at the module level can slow down import significantly! slixmpp/docs/howto/xeps.rst000066400000000000000000000035711477105560000163660ustar00rootroot00000000000000Supported XEPS ============== ======= ============================= ================ XEP Description Notes ======= ============================= ================ `0004`_ Data forms `0009`_ Jabber RPC `0012`_ Last Activity `0030`_ Service Discovery `0033`_ Extended Stanza Addressing `0045`_ Multi-User Chat (MUC) Client-side only `0050`_ Ad-hoc Commands `0059`_ Result Set Management `0060`_ Publish/Subscribe (PubSub) Client-side only `0066`_ Out-of-band Data `0078`_ Non-SASL Authentication `0082`_ XMPP Date and Time Profiles `0085`_ Chat-State Notifications `0086`_ Error Condition Mappings `0092`_ Software Version `0128`_ Service Discovery Extensions `0202`_ Entity Time `0203`_ Delayed Delivery `0224`_ Attention `0249`_ Direct MUC Invitations ======= ============================= ================ .. _0004: http://xmpp.org/extensions/xep-0004.html .. _0009: http://xmpp.org/extensions/xep-0009.html .. _0012: http://xmpp.org/extensions/xep-0012.html .. _0030: http://xmpp.org/extensions/xep-0030.html .. _0033: http://xmpp.org/extensions/xep-0033.html .. _0045: http://xmpp.org/extensions/xep-0045.html .. _0050: http://xmpp.org/extensions/xep-0050.html .. _0059: http://xmpp.org/extensions/xep-0059.html .. _0060: http://xmpp.org/extensions/xep-0060.html .. _0066: http://xmpp.org/extensions/xep-0066.html .. _0078: http://xmpp.org/extensions/xep-0078.html .. _0082: http://xmpp.org/extensions/xep-0082.html .. _0085: http://xmpp.org/extensions/xep-0085.html .. _0086: http://xmpp.org/extensions/xep-0086.html .. _0092: http://xmpp.org/extensions/xep-0092.html .. _0128: http://xmpp.org/extensions/xep-0128.html .. _0199: http://xmpp.org/extensions/xep-0199.html .. _0202: http://xmpp.org/extensions/xep-0202.html .. _0203: http://xmpp.org/extensions/xep-0203.html .. _0224: http://xmpp.org/extensions/xep-0224.html .. _0249: http://xmpp.org/extensions/xep-0249.html slixmpp/docs/howto/xmpp_tdg.rst000066400000000000000000000250141477105560000172250ustar00rootroot00000000000000Following *XMPP: The Definitive Guide* ====================================== Slixmpp was featured in the first edition of the O'Reilly book `XMPP: The Definitive Guide `_ by Peter Saint-Andre, Kevin Smith, and Remko Tronçon. The original source code for the book's examples can be found at http://github.com/remko/xmpp-tdg. An updated version of the source code, maintained to stay current with the latest Slixmpp release, is available at http://github.com/legastero/xmpp-tdg. However, since publication, Slixmpp has advanced from version 0.2.1 to version 1.0 and there have been several major API changes. The most notable is the introduction of :term:`stanza objects ` which have simplified and standardized interactions with the XMPP XML stream. What follows is a walk-through of *The Definitive Guide* highlighting the changes needed to make the code examples work with version 1.0 of Slixmpp. These changes have been kept to a minimum to preserve the correlation with the book's explanations, so be aware that some code may not use current best practices. Example 2-2. (Page 26) ---------------------- **Implementation of a basic bot that echoes all incoming messages back to its sender.** The echo bot example requires a change to the ``handleIncomingMessage`` method to reflect the use of the ``Message`` :term:`stanza object`. The ``"jid"`` field of the message object should now be ``"from"`` to match the ``from`` attribute of the actual XML message stanza. Likewise, ``"message"`` changes to ``"body"`` to match the ``body`` element of the message stanza. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingMessage(self, message): self.xmpp.send_message(message["from"], message["body"]) `View full source (1) `_ | `View original code (1) `_ Example 14-1. (Page 215) ------------------------ **CheshiR IM bot implementation.** The main event handling method in the Bot class is meant to process both message events and presence update events. With the new changes in Slixmpp 1.0, extracting a CheshiR status "message" from both types of stanzas requires accessing different attributes. In the case of a message stanza, the ``"body"`` attribute would contain the CheshiR message. For a presence event, the information is stored in the ``"status"`` attribute. To handle both cases, we can test the type of the given event object and look up the proper attribute based on the type. Like in the EchoBot example, the expression ``event["jid"]`` needs to change to ``event["from"]`` in order to get a JID object for the stanza's sender. Because other functions in CheshiR assume that the JID is a string, the ``jid`` attribute is used to access the string version of the JID. A check is also added in case ``user`` is ``None``, but the check could (and probably should) be placed in ``addMessageFromUser``. Another change is needed in ``handleMessageAddedToBackend`` where an HTML-IM response is created. The HTML content should be enclosed in a single element, such as a ``

`` tag. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingXMPPEvent(self, event): msgLocations = {slixmpp.stanza.presence.Presence: "status", slixmpp.stanza.message.Message: "body"} message = event[msgLocations[type(event)]] user = self.backend.getUserFromJID(event["from"].jid) if user is not None: self.backend.addMessageFromUser(message, user) def handleMessageAddedToBackend(self, message) : body = message.user + ": " + message.text htmlBody = "

%(user)s: %(message)s

" % { "uri": self.url + "/" + message.user, "user" : message.user, "message" : message.text } for subscriberJID in self.backend.getSubscriberJIDs(message.user) : self.xmpp.send_message(subscriberJID, body, mhtml=htmlBody) `View full source (2) `_ | `View original code (2) `_ Example 14-3. (Page 217) ------------------------ **Configurable CheshiR IM bot implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. The main difference for the configurable IM bot is the handling for the data form in ``handleConfigurationCommand``. The test for equality with the string ``"1"`` is no longer required; Slixmpp converts boolean data form fields to the values ``True`` and ``False`` automatically. For the method ``handleIncomingXMPPPresence``, the attribute ``"jid"`` is again converted to ``"from"`` to get a JID object for the presence stanza's sender, and the ``jid`` attribute is used to access the string version of that JID object. A check is also added in case ``user`` is ``None``, but the check could (and probably should) be placed in ``getShouldMonitorPresenceFromUser``. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleConfigurationCommand(self, form, sessionId): values = form.getValues() monitorPresence =values["monitorPresence"] jid = self.xmpp.plugin["xep_0050"].sessions[sessionId]["jid"] user = self.backend.getUserFromJID(jid) self.backend.setShouldMonitorPresenceFromUser(user, monitorPresence) def handleIncomingXMPPPresence(self, event): user = self.backend.getUserFromJID(event["from"].jid) if user is not None: if self.backend.getShouldMonitorPresenceFromUser(user): self.handleIncomingXMPPEvent(event) `View full source (3) `_ | `View original code (3) `_ Example 14-4. (Page 220) ------------------------ **CheshiR IM server component implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. Like several previous examples, a needed change is to replace ``subscription["from"]`` with ``subscription["from"].jid`` because the ``BaseXMPP`` method ``make_presence`` requires the JID to be a string. A correction needs to be made in ``handleXMPPPresenceProbe`` because a line was left out of the original implementation; the variable ``user`` is undefined. The JID of the user can be extracted from the presence stanza's ``from`` attribute. Since this implementation of CheshiR uses an XMPP component, it must include a ``from`` attribute in all messages that it sends. Adding the ``from`` attribute is done by including ``mfrom=self.xmpp.jid`` in calls to ``self.xmpp.send_message``. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleXMPPPresenceProbe(self, event) : self.xmpp.send_presence(pto = event["from"]) def handleXMPPPresenceSubscription(self, subscription) : if subscription["type"] == "subscribe" : userJID = subscription["from"].jid self.xmpp.send_presence_subscription(pto=userJID, ptype="subscribed") self.xmpp.send_presence(pto = userJID) self.xmpp.send_presence_subscription(pto=userJID, ptype="subscribe") def handleMessageAddedToBackend(self, message) : body = message.user + ": " + message.text for subscriberJID in self.backend.getSubscriberJIDs(message.user) : self.xmpp.send_message(subscriberJID, body, mfrom=self.xmpp.jid) `View full source (4) `_ | `View original code (4) `_ Example 14-6. (Page 223) ------------------------ **CheshiR IM server component with in-band registration support.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. After applying the changes from Example 14-4 above, the registrable component implementation should work correctly. .. tip:: To see how to implement in-band registration as a Slixmpp plugin, see the tutorial :ref:`create-plugin`. `View full source (5) `_ | `View original code (5) `_ Example 14-7. (Page 225) ------------------------ **Extended CheshiR IM server component implementation.** .. note:: Since the CheshiR examples build on each other, see previous sections for corrections to code that is not marked as new in the book example. While the final code example can look daunting with all of the changes made, it requires very few modifications to work with the latest version of Slixmpp. Most differences are the result of CheshiR's backend functions expecting JIDs to be strings so that they can be stripped to bare JIDs. To resolve these, use the ``jid`` attribute of the JID objects. Also, references to ``"message"`` and ``"jid"`` attributes need to be changed to either ``"body"`` or ``"status"``, and either ``"from"`` or ``"to"`` depending on if the object is a message or presence stanza and which of the JIDs from the stanza is needed. Updated Code ~~~~~~~~~~~~ .. code-block:: python def handleIncomingXMPPMessage(self, event) : message = self.addRecipientToMessage(event["body"], event["to"].jid) user = self.backend.getUserFromJID(event["from"].jid) self.backend.addMessageFromUser(message, user) def handleIncomingXMPPPresence(self, event) : if event["to"].jid == self.componentDomain : user = self.backend.getUserFromJID(event["from"].jid) self.backend.addMessageFromUser(event["status"], user) ... def handleXMPPPresenceSubscription(self, subscription) : if subscription["type"] == "subscribe" : userJID = subscription["from"].jid user = self.backend.getUserFromJID(userJID) contactJID = subscription["to"] self.xmpp.send_presence_subscription( pfrom=contactJID, pto=userJID, ptype="subscribed", pnick=user) self.sendPresenceOfContactToUser(contactJID=contactJID, userJID=userJID) if contactJID == self.componentDomain : self.sendAllContactSubscriptionRequestsToUser(userJID) `View full source (6) `_ | `View original code (6) `_ slixmpp/docs/index.rst000066400000000000000000000135531477105560000153570ustar00rootroot00000000000000Slixmpp ######### .. sidebar:: Get the Code The latest source code for Slixmpp may be found on the `Git repo `_. :: git clone https://codeberg.org/poezio/slixmpp An XMPP chat room is available for discussing and getting help with slixmpp. **Chat** `slixmpp@muc.poez.io `_ **Reporting bugs** You can report bugs at http://codeberg.org/poezio/slixmpp/issues. Slixmpp is an :ref:`MIT licensed ` XMPP library for Python 3.7+, Slixmpp's design goals and philosphy are: **Low number of dependencies** Installing and using Slixmpp should be as simple as possible, without having to deal with long dependency chains. As part of reducing the number of dependencies, some third party modules are included with Slixmpp in the ``thirdparty`` directory. Imports from this module first try to import an existing installed version before loading the packaged version, when possible. **Every XEP as a plugin** Following Python's "batteries included" approach, the goal is to provide support for all currently active XEPs (final and draft). Since adding XEP support is done through easy to create plugins, the hope is to also provide a solid base for implementing and creating experimental XEPs. **Rewarding to work with** As much as possible, Slixmpp should allow things to "just work" using sensible defaults and appropriate abstractions. XML can be ugly to work with, but it doesn't have to be that way. Here's your first Slixmpp Bot: -------------------------------- .. code-block:: python import asyncio import logging from slixmpp import ClientXMPP class EchoBot(ClientXMPP): def __init__(self, jid, password): ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) # If you wanted more functionality, here's how to register plugins: # self.register_plugin('xep_0030') # Service Discovery # self.register_plugin('xep_0199') # XMPP Ping # Here's how to access plugins once you've registered them: # self['xep_0030'].add_feature('echo_demo') def session_start(self, event): self.send_presence() self.get_roster() # Most get_*/set_* methods from plugins use Iq stanzas, which # are sent asynchronously. You can almost always provide a # callback that will be executed when the reply is received. def message(self, msg): if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Ideally use optparse or argparse to get JID, # password, and log level. logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') xmpp = EchoBot('somejid@example.com', 'use_getpass') xmpp.connect() asyncio.get_event_loop().run_forever() Documentation Index ------------------- .. toctree:: :maxdepth: 2 getting_started/index howto/index api/index api/stanza/index event_index sleekxmpp architecture Plugins ~~~~~~~ .. toctree:: :maxdepth: 1 api/plugins/index Additional Info --------------- .. toctree:: :hidden: glossary license * :ref:`license` * :ref:`glossary` * :ref:`genindex` * :ref:`modindex` * :ref:`search` Slixmpp Credits --------------- **Maintainers:** - Florent Le Coz (`louiz@louiz.org `_), - Mathieu Pasquet (`mathieui@mathieui.net `_), - Emmanuel Gil Peyrot (`Link mauve `_) - Maxime Buquet (`pep `_) **Contributors:** - Sam Whited (`Sam Whited `_) - Dan Sully (`Dan Sully `_) - Gasper Zejn (`Gasper Zejn `_) - Krzysztof Kotlenga (`Krzysztof Kotlenga `_) - Tsukasa Hiiragi (`Tsukasa Hiiragi `_) SleekXMPP Credits ----------------- Slixmpp is a friendly fork of `SleekXMPP `_ which goal is to use asyncio instead of threads to handle networking. See :ref:`differences`. We are crediting SleekXMPP Authors here. .. note:: Those people made SleekXMPP, so you should not bother them if you have an issue with slixmpp. But it’s still fair to credit them for their work. **Main Author:** `Nathan Fritz `_ `fritzy@netflint.net `_, `@fritzy `_ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP `_, and a former member of the XMPP Council. **Co-Author:** `Lance Stout `_ `lancestout@gmail.com `_, `@lancestout `_ Both Fritzy and Lance work for `&yet `_, which specializes in realtime web and XMPP applications. - `contact@andyet.net `_ - `XMPP Consulting `_ **Contributors:** - Brian Beggs (`macdiesel `_) - Dann Martens (`dannmartens `_) - Florent Le Coz (`louiz `_) - Kevin Smith (`Kev `_, http://kismith.co.uk) - Remko Tronçon (`remko `_, http://el-tramo.be) - Te-jé Rogers (`te-je `_) - Thom Nichols (`tomstrummer `_) slixmpp/docs/license.rst000066400000000000000000000001121477105560000156550ustar00rootroot00000000000000.. _license: License (MIT) ============= .. literalinclude:: ../LICENSE slixmpp/docs/make.bat000066400000000000000000000106411477105560000151160ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Slixmpp.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Slixmpp.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end slixmpp/docs/projects.rst000066400000000000000000000063161477105560000161000ustar00rootroot00000000000000Projects Using Slixmpp ====================== This page enumerates software in the form of applications, bots and gateways utilizing the XMPP protocols with slixmpp. Applications ------------ sendxmpp-py ~~~~~~~~~~~ sendxmpp is a command line program and is the XMPP equivalent of sendmail. It is a Python version of the original sendxmpp which is written in Perl. - `Source `__ - `Groupchat `__ Bots ---- BotLogMauve ~~~~~~~~~~~ XMPP bot which logs groupchat messages. Logs are in text format, with one file per day and per groupchat. - `Source `__ BukuBot ~~~~~~~ BukuBot makes it possible to manage and search your bookmarks from your chat. - `Source `__ LinkBot ~~~~~~~ This bot reveals the title of any shared link in a groupchat for quick content insight. - `Source `__ llama-bot ~~~~~~~~~ Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it. - `Source `__ - `Demo `__ Morbot ~~~~~~ Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs. - `Source `__ Slixfeed ~~~~~~~~ Slixfeed aims to be an easy to use and fully-featured news aggregator bot for XMPP. It provides a convenient access to Blogs, Fediverse and News websites along with filtering functionality. - `Groupchat `__ - `Source `__ sms4you ~~~~~~~ sms4you forwards messages from and to SMS and connects either with sms4you-xmpp or sms4you-email to choose the other mean of communication. Nice for receiving or sending SMS, independently from carrying a SIM card. - `Homepage `__ - `Source `__ Stable Diffusion ~~~~~~~~~~~~~~~~ XMPP bot that generates digital images from textual descriptions. - `Groupchat `__ - `Source `__ WhisperBot ~~~~~~~~~~ XMPP bot that transliterates audio messages using OpenAI's Whisper libraries. - `Source `__ XMPP MUC Message Gateway ~~~~~~~~~~~~~~~~~~~~~~~~ A multipurpose JSON forwarder microservice from HTTP POST to XMPP MUC room over TLSv1.2 with SliXMPP. - `Source `__ Services -------- AtomToPubsub ~~~~~~~~~~~~ AtomToPubsub is a simple Python script that parses Atom + RSS feeds and pushes the entries to a designated XMPP Pubsub Node. - `Groupchat `__ - `Source `__ Slidge ~~~~~~ Slidge is a general purpose XMPP gateway framework in Python. - `Groupchat `__ - `Homepage `__ - `Source `__ slixmpp/docs/python-objects.inv000066400000000000000000003165461477105560000172140ustar00rootroot00000000000000# Sphinx inventory version 2 # Project: Python # Version: 3.2 # The remainder of this file is compressed using zlib. xڬYH6zϯ7vc$Yt. U h-\ן%IRLWBBYO]ewG.df<,ma5eGIhtLN\Q9 }!c 5pǸ?a,O|I~9r-쨹Y%牳Z̏Tm)~ MK~i{}{<ocz)!};N<)j>k: C6[8sUp80SG uO/ŪFgt md> gl(}E )quoP"nH?833Sߢ>K{+xf*">~gq&Cz8Jv˘fg[ؤy.J")ab"b[+O q:4_Ez-s>O~BFW_pJy_nQHaj؞ӼLf `|Nա8dq21|{3';xevAߡzܟ on<YGQBnc'E-]Ҭȃuwn:̢Ml7Boo]1_%fY~tL 5%lTƩ_|xDxwtٶxtGR;XOp5^&=-֑sld!r$k8'a*ΖŅ⦳[n-8[x(3;鏦?wb/x(0|ӫ{&bV9?X .nqu'N! DA, S{o8FXgs<4+| ^xy\)blŞ!>rK' ǭod9S1ѥPPWӀbx5 2KVũJf]<, UzKyJ1hS߶yxO\C }8"T"E|MFdoY`ޝ"#|~^DŽQ>wS|'Chtݍx8(I`GyP8D/$o`.dZo!/o4 's-BQt1a{j< ?:'ϟSp;9:~Y&aFK7T? &iE;v_!>[OkjVHy!?x+RQ|tKZ%1 .F_FP*:BmO}M_FUm'lPt%Uw*/.U"Yjxo/m† /=aºK|>oMrX-S%1PܱL7YغO PaA ,,O ʞݍBjs4}* M$Ҙfd'D, ˆ1G ՀQSwu"x!.&DϐǎϺ;Ty<ބvdG$}t<%o\88O.' aPWW?XZ~JpmIp6xow?Ļ;7&7K :jٶ ښ/唉*⥗ >C8"cU{w\v @`iB/n-<,/RhU;3Hap/yXFwUP&Sb Cze6$'Vp̏E*qΜ 1zΠ"لRsF/1H&d}^Ts(-h#Ҽ{m#Z|#68_ 3pȏovz7XCY)\W/-dwY҅sW9:E P3}#2}>B†_0"NVxHS?05B "dnof_'b1n/,:)or*ErgY~R{E{XÈ\z@G9@X^YBZ;Z BO:_ 11 oCJHu͸?m>nx3xPq_Ei}/G ?hSr'E##xI79~[ݻwmًl[jֆ8\ 줏MOF7q߾Ko} Xqx^9~V[7qрK>+|ekKTԥrm37HEg%ˀ,> ɧM"$J A˃F!F)n[~| |4 PyE;(;--*VO/^^YF72Z Po|{G➂6{?UG4?2(J)4Dpp*,pXMUas\!qC_ך'ز$i"y穊JhI)b=C7nY xyJE R#&f烫a.ැq'XK[Q#x^rvԇ..SAfU1~M{89`R4/1",ᘝ'*xcQ!q>â^VA0)`2SNE֊5VB/xN d&B}zdvYtJ \m R7aR5J6 7Ȗ'QI0^0DeBov? 4QD (,}]D~u=qvBf?}0p9s{h77gq\lث #!PZa߿A=<ê>;-ހO)}~'1ɮ/mۑFݪ T1w<=(D7)Up,M=VOyObk@⪫T]WmNuo6=7WW,d Q\R)ۢpU~-ľu]Edsq}&+1aE bzfc0_8vph\w{H6i*q]n*R}?aA OkKS,KaXC )5a!eA5Ѥ =~eyMvTrLM\(Il$^`.MA*kqqӛ/eOJBK_Κ^-4 RoBCN2+D8J_ƟIZiC!D_LL$n? {Ţ#&{n;XVS"rw)^|-Xf^* tQ{IH/FΞ]PtpgEzAH8{ uК}-\U3VQ湽ji-–ڬ6Kl )y)5q|W&G~R,6Da&)l&wSiyXxyPmX -wueF7ΑyI]*YTiN*[Ϲ C'v;'4Q~Ux\û .;fIٿ\ qʁ2[vcenR*GR8W@ Pg(N>Z&qoYB oU~>˨Q'tC塶+SE޻ح~H.wYS<^j`} p]kҖ _'6>F@Av(8 3Dv5G!g۔E:լq?P T,WxFk09n`wfUrhU$>φ$Kwl' et;?'(k<w?әXREw~t7w<_lK_Ҩ*8 OWL+$f](!(*r SvwU _(.AEu$j[Ast,a,ʁf>Q B9X/4N6~wg6?*`+8ךVRU)p@oB_XFI\ 3 Fgk80 ^[+BQ+cm_o6уi!F+2RPiP24݂5UbJ/_Io~ s+ ] OU|nUy5 ضk}1Ф}}q{6k#ؽ0x `?Qz@ )ź'܋I) d  tbC E gZϘEUA')nK-7D7u<x_@ݙ$Ê_qR>~v/BL'1sv_`pAW xP~q._K7PR;Ѫiv*\M_oͶD'pq"6TH*Uz9USz>ȻL2kj1}l8)RTϨqV6bG(]+JQE[Xml@d)̭;' )l.|okQWw9i=q̣RWo_;aVu'[Hd7A`׳rCp2:75X?6O 1~sthl>=2J֪δ$_V>>C ۔/*jJ^{fBn>fh>؎4p4<(繰vn {4~8R.M&A1>o /dfSTxbuWPGfs8joSglO,>c0ERZ #AZ ]ûkgNy|g7p$Y*Z04jHXaHNI!]#A1NgN`"9N^E-_)D>XNȞmhXqI# ӓT,m[n6^a޾tkexp]C4%ȌQH1Iv.6߀i]2iN{oPHeC5*.*@ WXT.MZ8_zd~h}64j2LPB=(M/C82Sv"}mW~ GJOU.p%{<쵫9&^EQR.A)q%x6}?vswuEOԤyDpeBZPh W\"r`^9L=b;ޱN?0q$Q<nj]rI0ez)(Q/7RXJt}g"JUkɠLKr RD5Ss*e$ J!fV8TwsGSe!ψ`9m 5+ʍ%C*bJ `v%nsC"{ &]`u(O[04⟇B>IH ]FPhHjx ++IH Pc"]PLPi?|^,U_- I^D`oY|_u}O:̴,1|'F9kQڅ )a\y99$ x%4EU`=HR](k΁ڡ ox׀eH{*E{yF# $ ƚu5}0N#_*vo ӑ/7tv|^|Jw]e]KD4?$ | DXE OF>T`{|x5H ۂE4TJŕJ՚ވOQPGTwjAq!lc(tT˚lN ,X*[|;`{7[ %+َGgeugq@R nA ߣD 7Yub +1Fy¼]Ō9+z7r ER~Z4LtV9CVz3Y僲,z{P#"T!cvi;|L[|ktP~ƷմH@(&䟔5ߛU!MA9;=/ EC.x4Fp8QD }U*# JM(x0ӦZ[mf6QAh;OHBb|yïgG\Ȥ< a;.Q񳗡sVxNO@l+ف~\J'Z` ?y0YQ.vm,>Y_rXVW4^U-ҳJJ TqR cMyeG-J XB/q,RK;+I|(I F i}(,m)`{K7u (RL1fS*71|[2n pN\C,'I?uq3t>)@FWuBVf4bBrcUnܥj-u<( pKtIk’AIĖ5BWM*_&AVjdm.lM FX_0s`_Lmj*| umHb8BscY]QXAס~ˇA|80C&C\@i,}w:| iٯ"?6~lB!x((X%8-Vni&ȮD{?4q_9O"t.MH< pH_@ɨ2\c\oB^ 5[?5M2?fL%7*YeZ)ުңRs0*zEkLl<8D^ œ*O(\_O*U e; ٭s5S.ͧLdAX*KrSrLcN~2i EYVn3/jEtU801~jݩePB(籅9L?ɛvgqB*BŎ4>kf!^#P4~63 qA@@U|]'={~5 eJ O!u:(XWG^ t C{{}Aqݿ.6\I|Mij&b)K[cxdܿ3[LNa5qi,-[ .̃:.]$!`RĚwQοE`4Gh#_0 $ xޠy.mܰ23BcQ} *#~aZUe6לr`J1&#( 80LbYn1fh NBNHƒ\GV%j p{[*a}Qd!q!zDŻwaG9_xIDJ^GOnIo%k󜯣e,𫦔Fb u3"A=}JG 'iK Mfb  e ^))|O {`/͑PGY=.6lD.PP{i!;I(3iX po2 e=zU BE)Sz5addYdDpoGPYr=?ֲBC)YX|^'*A8}Nh 5WWVv2g$]zFB԰',vx{]nSl4{MTK=6gۘNɇ٥dB΄It@-{vkG^q[%.1MHt$ 2;R F";;zh4R2^sί_jPڅBAؠ4y)5QuHd%P<;@hn%!iGWܺ~FXV#}KߨSjءjxPlYLofTُ虀}{ͽn惓"^'@ě7ʐ'*? . [')JJv3^(.޴&Z)c' %HMpFp+YŃ"g"ruCd™2a0SEYސAWMNUG=q0fidȇX-^9g#tptU'Y(BHANuȻ-X iّx"1NmT3=7maE `@hC͋:tͬUWuxcsuVŇ?V&N  q]/EVVͺljj!'CNM4%x &7XB[;Wmv2%3IKYρXXu%Daa,r$g1bד ZƄ/Q:2$+i5Oi8~8~8vOZ|']U>)iX6c*x$&׼BDmH}b!YU{ U{ϳn_1]Ah37喡:'$ ]eWi7~.0`S?Y1u3Bs.Kw܍{ (A8!pz H})X5Qhit|9AI*.ŋk*K aݽ r3`ȯbo={vrJ}IQ8U v#H@zvK\bFMAsNE⪉aLB$(%ZΫ6GZsvޜJI.svɯ]6bQxA? mY(fR٬3QɧzE槟$"ܒaY-Wl9ILhH"(_9*h\Y\McxF o: 6@ĬTԧTS)^B %(ߊ7 (cAH4]񳏦EBui*(c19"F$a~҉0ñPF/_%'ǯzT:f쩭aKGӇS*+۠ ~5()'Vͤ_4CВ<'tS<%Lk0{Vd0",?{[Nty9BK=.M IfwGǣCJ)pb䅰rCl"l)N!ѶǡhoWJⅽyR1HkńdzR>(ߋ{kǃ -4 * 1KLЅќrR*rA:M)&EwmUrf] ؒ2r0Bƴa%Jrqs~RhCxTboř31@ - ~'` //:| m'_Ǜt¾ j٬z5Fֲ@x0Pa>G ,:u.RRL| N3 CBG5uTȐY+Gn"IkqZoJyM2T iE,VA!kIºuDk{Žhhw;8K4F[(I/' ׽ <č!R+a?!E֟Uϝ0 bu(tV`Ra]hJJ<$ UMۧy!uie$bP],AW 77dmxXUI v@/` P0bjuAʜVήa r ~My_1ZQ&4:dy[FJc"* 4b5j_ M4i4փa/Epq[0Q]aB2'%?●Ϩ&aҸSz뙒I.1p )3ۊɋ7 hm\2i.|0%1nWOQB훌杦 ^U#QDK J s<3[Ugwlny{8$>uߟFTGl.|.8HVp!T:i:aٹ2.4#K,<ҕ6>5!g;nPU?!5SCXy(-RѴn]tZ.BԖiy0׸w T2bT=`WRb {k̶>&Ӱ b:ZzU05V`;쒗@l4"o4ѡ{椖g|ڰjw')" ô@-Pi)1@<HoFB^;nrRIZē".aJJ3(^0=GJ뙌pHQb~&tz?AMRg]%.1a57Rlٷ<\Ϻ,\I<"915y.n8 <NvC PT |+5?ۻ2c,ͽO4gR*孬~5뤿<՝@谋J QRݥNJ(1wa 'ʠk* , qYn噃! ƊTKl-xYY`Lq4&ϱetvVrraI w9!jLކj ۪+%d I 4U $GMzdU/t2CjMJ\JZH׳.,_ֈ(/7 UHػm ͧU$\m|^º(}v#v_ᐵi O;Ke G&VqH_ݟu,Un ŀۆ_p~zMjG 5Zfm^eVe~wɂc]2綍#Nݖ4Ri BJfMDŬ-3B{/wtנۭ _o4؆XxN2^ kB]cbFm&B)=;T@`n*N|gnn3*m:{w:W@Bb|/K:v']y&mj/5ISP+{u&B ,R@jM$a[&$5LU˚A4i; jpH%#4Vq&X*~T C~$u*%TZfVрrjsVF-髈Y;тM:1~JٵXduVɅKjRK*uՆߪb5(' QRk166Ak[ˢ,SkZ &l4tЈR#lxRRP}M3|1GD&Ve'H oi_ `V6:15}b\ Oq9MeC6_ʲ*_aJgp|t>% w~Vr@__zZbM;8a:+L/iRvDy%qV_">>G/7F?Sm8`mb6* Ă2K]Wڨ`PI",Iw| D'߉pغ%{. S79T9CzmήWQP;)ܕֆOZp0%8V["gaQ\֢;ݒ!Ȭ T|In[gC5k3˥g- 1x#[$Ae6\Z/$aSΤη՟oIuǪ `lw _>%a2Cg)m{h8/4HZckij&CR;2a籍窎x*p+Ky>d7Vl^JT :Cgq1ɮ5 +IeZ#qL 94!$OC'33xU e&$F#LRg618K+A|Ģ%y|ݑNXOG,܍,lpQq gw2V1(,A3Xc0lb\g%,$|^^ Sx%jZ6!W@R+ؽ>ے0O@:25.@.ySu:; 5YjE2PL1/U6ƶ6v6ln;by](IY}\C'2>JGJ(ƿ0vcoe܂"F4Űb;a$v!y2ALy봕hJAeϾ:5)(]4/q-_aP TTS/JMe@TF]6}4 [م>2o cNu$ P7Z5n o %)!ADZG~nGh'}-uӯ@)OhM I:b~nWhdch9Xc⠪2Ŧ\1G,'dI^~VMvSKnsq9A|#EKan㲭0A_UGCDbapHƹs)?#մ2# ;t-dɡuX=Z=%D̥"hNȺ8^-;["b'5^l@/SZV4 rt_͛fiޚ/~k~+S*/CA ƦWqa)#01mwaOeh~RMGr*9]Þ;j0u;9f,FE k@<$ }BwғB%\6]md5?lFT Z8UpĈ (+٪ZsYXu,}u@MASyQWtȐ̋S Ype)ՏJzpΝQjGft>ƻ`Ksjo6FlY\c^щQ#np"󖶠d{ա {,4+NJê,Ƈ-YkUJGIq'٣{>Kb䎽1ؔC:xOr1×JDzXm.fUaM㲀qPb8?Ϯ 16vP'V(r 9C+r_$ߏ$@Ju|,Fݬ*E] ."/$f3|wGCn7nƗ}V3o s#QV,jvnZ<`RC9!E-h0Utx[r4Ƹ ?PU5pn;Q6_kE" JU!]+{  ]sA}/fk[>`FE5'5,Z'(xFԘ&rT:!W57|߸Ǚ4e|,'XkCUT4a'kWImTJ@J6BrM&%GKpGyE]*Y66rj@2+BE{om(,+,$NEݞa!fq[*ZUq&_p#AW 8.)LWrɈ0Kw:߲ɔ3Fhh5ycxɓ^o[rPm,+@o l~'bZM%C[ gE_dcnJbO8|w^V!nBRPG%k7Y1Av^ykX;Ins jyVkOµb!A^?-2kFÈiP,%ga.\~B^M<>R[GgR g B ,= , 5(9Y*e5S,m0{-jzYj',%sR*Mo*a$ms8FQ_e?"ըeYǍϺdYxFWWBLmJPyF!r?u{a E(be[9 RX{p`#n+"- `>6~GQ^./x3aآW7%TPn_܍/b4t.L>` 2\=)~^BT*e\Rq<(cTN)Fj7Cxň}<9'1ㅅB yC$cD#8;*P Hs$Z0MQD#Ԧ5ۨfm¦T(&*1Hh"KH1$^ w{ݾP[ I Y psc{ڵ?z\<+Pnjx'K}z}jܗ-r7,yH ;n, g (UٓJ9UD=hZ,Uv0XhݯoMŶu*ߞ[q&=](WmgNUv\ǯ48.`ֺ %[{b8XO|:ߡQ~)1R ѯ4.F9T[H2nnnzi:yXn-9.as ݿ+ K[\eb݌hz7'?ta/OJB6}.U OߕVɜN峧(Så9^/#^Sc ~ɬ>s_b-"K+C<WZYm+ˀNx@L͈Iˮk ~EΈxxSϡ8JVW߽Bz2A!bg*#amརŸ h2-¤ ;[NIKp͇IOϰ sP_GQ^ûD Ïi $FSC?w)YaFVJVGcyw9xt kѱij@ݪ:5s6hZ sv5||y)aK,gp;MїѴlTb D/JGhxyg??Z}=RWaVu2 )1Co8!!nUE ݆8¼He))8M6,1jht4:NQZp.l1[ vBpv/"'\u,^4LQ\DZNI4U(F :zHn Z󃬡t@_f6J)ŇyIe|nZu!c@A%eCk VJ:cC[7ƠʶQ 4cԒM.Fd&Ψr6Y׼]Ă}8-Az&' v^ w(ҽqե)޵884ip?,C孰a(8`$`]6ӧ0RGw"M@(=86ѫ x`Inܑ=Q>)[0=Ux9 EcFמ?n k>LIUkSN+Y;?a\qHd=Y_/iW#P^UR=6m%|0+X0|Ҿ=xi+yzqAXb d7OSIGyc-X|Dgу_ב3lQYFpy(7 89aBpKS2XY9 ۙe$N0h?zE M96Qn8Ij\$aXGvjhoi+L%&@{٢Y\jyBeô?F!.ejI ]^ju}OB"ŵkob-n@*8Fa VJZ7~ɸWq:*6.e9Q! jw3!CcBcr/G*Okk *i#1D8]ƪuM .!NF_ /Wm.@JGQwW_V q iWa%S({mS{/3',N/ Д$j;_R5 ށ8 L`jNӯŧ\q!@aR[gG|D`pzT$^yjJyûO,,/ⱃq)R8 {s80?jC8?{4KY q[I? c}NQqLh~ ̜I+\>)g"Pu}/x~ T:l'G(*o1M/#]{D#!~0-9ռ#ۥ; Sz4̢ݡ{vb7~ϞczI;&fknBf^fF4W13| 1pd#jL﹵VDn(Q Vǻը%r! -,:z`0v%be>~ l6] w14yANe~epF㪲G/LS4fmxGaU!hen`,2]F+S*C;ՁAI7m0ӗ(3P  m~7fGà D,JTi jީZ!&DK*Ipaq pp;yX]2H'^')!Hr%,">ɡOT4KН+a|8{Zn*=ô'R{f,z-~™cbv9X z2?ib`)3ˉgW^<ѶBl*ZvLx4kN+TZyT{*%$I/(nzW (v  +Nwڙs M+f6~u_F;ɵ#w3ۈgr$~$ f/rMnɝEUf ߼ѫ/p]gj4\wr+jk[*^}+lJBděOTbrj4G=ĹP=W @|X_>T\:QW}@q0v"W]2coyQw3:8\406*Y"v riH,cf [8Yz1J톖wyJwjn-35_0 ?])H&خqyN٪p@*Kζ`^CZWQTjaՀ學-h[UƬ1[&OB }r7 3ٗ|8Oם ,FxaIl*rmJm>6پ$F(JlpŘ]Vq \70AGqmrZh*B=W[L5\:WƲLvBZ(ԸRYKxq j^FKb0xFqy]; eoQUhwъQ>\x`+9 Ҳ#ޜf:ʽXa ހGo#B1 x+ɢpy2܂׺xC)vU7)%¢&zޟl{@cI3*i8eOTRz3rֺ0_TL)4!mh9L.j"uh~<sDr`³AKX+G7ww3FujC=I {ē4~Vg(q"lcm?l҈^(DhQJ9|%8rfhwL̥o怮yƜ>J7P5'Ƣ|9L2 f|إY[|׆@w2z@¸ۻs;t|5vHD4i|WZW9 /'toy@Bظs1ق㠤)>ky6 w 8v'ֹ6ԪfttfE`:4e6]LYOp|3MUpws? iɽ1-zm_AWG-m~7ǤPɳ7iF[d͛;O_'gҲ_x cpKyɾ+w=WP@AeFvL=e]U.lUe)5*_'"DX/5TJt{NIGLsEcV:w8m:c hJ>̻0x\+SK?Šl0j _y:ɻ=O6]jets1 /9h=K=0MD\]LJRFnZT-hyrAPL:㟴/[Q!hv,mBO"{"|;, "Ka2q5mG3~n!TQB~< Tg~wbIܕdT&DK,yy5Y(B0>-M!Fj^rؽx4p: 607oTqRo}f7^&}jIK4#/OOunmغ%_| TW9of(3{KQ[`V_~^(.k^c`v$gM:t`rʋaqp틯ob7,v[. WJ}kJAJ8IJ$U+Q\K]9jZQ-Z1$W7u-`W^U'ق̲vY^bݪ:[!L̯}XeOL\ƞU^bs^]L=.p: ɰޔW/ byb*y[<rF=ԊGS Dqύ^Z*@;6csj`މ?k_&"ՠ.)B55/-vi9Hm1fGqC)QnKV&|PdeMDQ AlGh4qo0'+0?>%ONb-&1w~E&]s ȮTWuAP jݪCn/㘲iFҲfQ&;{\4~] @Ǩ^o:4X6 R B_.a]{9=l*9z?Ge3wZCQ.Tw$0jdzG$"HN 5wB֖^=PٚJRvGH!7 —b($~Z_6f#sxIk !&s@$O P&g)ߡ۰uRXi';b1r$kKtUXO =j.%rnݯ}X{֭ç(TXT^6Y섊e`VѽI <!g`[]e}RчRKkN:f FZ*WdT\Uڢ^("~=qڒʮn:GJokaK@irtՐ^"0)eREuB *-B1ŠpfX Z@BLSȹTK|!x8aVƣV8<ekz0Ÿ3p-f\Z`}+۱pm!PtluQB#*PM$I=e2p 6<4иZ;s EخR529n`0i7fs}S1mꐼ04Z+] :i=GywsP'%1h .$/^_aت7bܨo<$ Iwp *]7G=A'Y%U)r7R0q/em\xXO-cBE$M|VnN cD@4Կ% r( cS8gkqwh^acఓx%7&U*} _F;^O Mҽ=܍uv$0BH 0AϣH.G 4AX Y>3k 8G h}!#\L=J{Ж*7TQFAsfhZ;[G_ C[HY\A7rz _AF#M\dZ)JThTnJ܎)zeuląÃ0zjAݚ,3ra:XQ\eja,۹jH׺/ +тٚD9j P[:|gngC=Uq PrC=3jҮiԩfv~ ЀIOj1z^t2. ]?_ir,1y<5PhH'Ty^h:Nj۹coxEߑ=W1j>:$3Nb3(s1ck ?"ЗSGkE{vJF> G98!.A;pĽ+)o ?̓h2K I^$wCnn,C#/qs]꺘iw8Ej /^9y%EIQI\tjtm[-,񋖇<~Ŋ | |ufr=e;nm1,PG.A/䕗8#hԚ[1&ܻT?q#Խ@N|?z '{`@+b5lcݼ7sq]d!a BF]<3g!p jckD0ĝmM=)J>WC<֓x+idO'$HbZL/e15Y5@}H@MFnhUsgc 5*FJ㸻2U~+CMQ0:tӰ2BHO3kcq/J`?SQ\V̠9yܽɓ"g| +AhK!pH]n+{'TQPEˠ&6K$IwQ)>)}F␻>T;Ed֐-1dby+XJIq!n1#[+E"%j/̏yݳiB5{pRAf+Sʄz'qdAI8ீ#.B3Ln'ɭ>UDWgW w#ϥ"" եKK}qW3D'<sU;G)ȞL,ȹyQ}͏u ]cͯ9Eᴇ^~B|SbliFUYz`\Kb<^Q}?BiڿYL8ڜ/QIkgN#;`tupq]Q8Ū39x &wɵ[YY}@>(&cwe‘.̳L?.H{=py\*g==!j".@V͠V^0:y K?:PKK>iEs x‚cn-s2-1. Кup$Htb2qw#XQ=>ZLŏY%9mwv^ssR[ `Wp! jKY:g_3@s;xc!"-DՅ^wZ%㖬W:"MnxvIJӊO:. d\f8/)ʸឋv|0OȾ=qZb Jr7LD۝> YDSPRppx@~?^?^u +5!:w3ȉ< <5KP>Rxcyy/~v_ɁlF1q12 ''ׯqb %1bˢwUeTZSqv=Oݭ 3 b+6}0qeq/=w&swض! } Q3B9F'9Zv"/Aw)֓\g $l{Cu5YOA{䓵1:㫸,Eo=0/tynjG\*Yp'\Y;Lݭߑ+[%U9^ bbȍ.bNNÎیڏ,bD81xn+ցwU*&e|\7ߑ[h WtY*ޘuV$)A4l;RtMf03:'.1r0W܈oad M#\?;bb!c^mI) _:Y\Y=}>E%?~Iϋ8&pV1;-UW35+j d6u;_KNΔk`K:yISZwzv$zd@|jw!kn_\e\m^jaho2Y4S~dFz+qj7wZ=>=?պ';D)嬀v ;ƛ_M#nڿXOXSw_ݡ4c4c5vgغ_ l'bmbǹ~!.MI$i?4t:,*ް|^5XS{Q^mE6v?@AX>R| |AEEEc&o|wa{*iU{-NVQM%2؍]x-"!5 g ϚمZ#3#B`I+V5ueYw6׬zZ6q8ooiȁ9޲&8ZdB M\Z^ .SDP,߉?dc=hߍE_24]꒿Pi߂hR_{{^9ț=\'}Kf;Jz(jxTx~AZekȮ.vj~vy\^ՙB̆)Ó*/lkkm Pjrev!)8J{:= Ӽv\b4Dw^;-7t/DDdQPWŧLuXG^*]_ w'6͑oj*DYOL:=1R9[ndzxl6 yWP&b:_a3@xow݄O~<@4'W n #yf~AU|$[gnv|QD>F_:_r,S^YiwAX-oãM`u,in<!{2& x?>O=鍧_=C3y^>/`Âb[<'`^bu!q̈́4 (Vn_ϑ&ċ2eL>law*N6W $Րt0ԇnt56fm}`)!'6vzmFqzY/ b,`ڃn |`+a~%i_S9ڹDaٿlaxkªR s(P YG=37щEoQ@9"*xUZ[6-D ; bj]FLxv(0{d*ni2ugpרc?fƠ?uPrÀ t=+*UAn|U!M{A6b_ o+26#2.S絖."qD0@ZFIœ {/g\$"xqeg")NNAyT m8n4C 5Fg=X_2(y?S^?oi YT@0U*UPnqV~ laLLrxn^H,6lvQ6;Y'ciZڢK$Pp&q./mRH>xLB pcq 3DWotYeO-gd+Ki#hYrͷmTkn֚÷l@ 1e1oP))Bj0͆ND[E.%#^'q6Pu=ّoAJw}"fz&<_-Acmg7bH9q !RŐr4 ވí9h&*@}ނ2 Hָ Sk$a3hv+6c?ktlȳ$ޥ(B#(=GjexhNq5nd]AJTu > hb딾K8;k=A}`ͭt&qf%p6n,Ch..)UvGEAE! Y49Y qy+.mn$BŁٴ:#ˣX:uD=}(o1JkV(.Agg\>'Hۉnjge_³|\KP鏆uOUZy{'ASk謳&EW˿7"W.tE*|D`Y, ,j7VÐg b `# jt?,R*T_ W;CѬuF-ɿm܏(.U5oNSHNKCd}XbB[PV>闡s0مoPWͽżX}+i7o9Zty<10lR!v5'юz@P}8ӆ͏\cl9x]-{PVM1̑`~ۗfpM* N7; γ'h33/%҈Ҕ:/Bֽ]@- '*ω> =%뤒)`Ϟ 1 wy_sT8:*vA+v,)jz:oNPϖ  Be,lQuðD@AXs "j3}  sECgYطJ88tUww?#ú3flYGEœ34鄶B+Wo_dsѲI{HRm[[|V,Z,L*FAƅnZrl tAimJUݍC[a+muIqdZNw(c_I9>&H{IhAIМ(O$H E9r_i*Ճy@g3r&tkVKP#"$w1%y#gW(Vl)a$CV,(ԟʞ P֨D۹h9YPB,z;E~.7\z9|y:r|%ӺʯX&z!鑔}z?yYUvuM#oTDG>dC0:ڋ:th;@.^.dzĎӓYݷ9SV-즱ʷaIB4T{Trt䏡53/ v JʺawbL}.52o^V OiGnGQ[h[KwJ^<~]+$[U@6?~ނ M` [;$VǶ2=L}T|(=Ahf3=zdj (ycH1%Clk78ɋ*i: k2IBVUO㢍{Z}vT,j߭QQ%V3m"}u|kSaz"g j6H(t~䪆j]y;UOhMRA`ɡtzD#'r'z{ނzn*J BzmwmM=_2D] [[:~.kiO9z8jL(bv}D/l1ľPf3fv T"Yyw {cWYc$-?` |&q' J2.Áfd/-S]B,F'PP;<'5OV:"z@ 49k`]sK}+R_e=_<.׋IR؆c&뤬:dJя!sgb79͌Ey.YTQ%jOP3(VavWNtY{EN< Cm^]##bu?.@"8&)䥣kihX JSڧ,6ψt+F n̻Iʯ։̉e)v_ ʮfT<;yQc7*~qmp=3' ٲFo7ӿ_so %Ŏ,:q̦ʧExf* |4HaTt`>v6{j8c/| +X9 ZJ'̂f|[6<0n/D٠ ō zW#8?܎˭ĵ'r_Ca")!ovÙ(Ms?(R[<"{_K5DTAZ\Q:%g2|U̐*% U"lAo^0X{@} B# L@JSnG' @14yi~r\u{-hi6db_csΨy䙦JO> B(揤!W|B 6x.&TkX^86_ r =. *&P7֡VYQkE#E^Hv7Kx`ze~*/ Bm8"j Bui ܢ~jNޥ |0Ջ $sV>^hnvhPx ~T#o%?& dje$ؚ*cQ(8'zJrb` 3.Uf(SLS ~*E!ܪ[^ 26m "vR`f v0d-̏`3j{M+~:&^yK:rӧ[V =*P"ѥ[&eaԆ>oց4 2)1Q| *4 RdEl XORF@ \[@=NJK.8 zHqgN@e0IC:'3uS 5gU~LQNX\& XkY6pDR1$`"L١ tkq84hD [E*!8(~Nqyn<)x4a=)UyRU AJ▄k>߼n]}"ߊ1y$;[!P%H9$:}(Ɏy"ZN `x[Σn=Elh ̭lkx,K,)߳^o\5Lj|'>]ijK$KMrJ3.N)OQ954nPKh%M6j=C=A:|0ML^eĂ8knTH#b^-_|==/KzIwxgKM1nm7|Ђ/w1ߞ+d&79Pdk>?xYƠQ4虊'0݁mf; l_dx39{-HZdD,'S[%f`mrVK%,e=lr](yZ>Ay3}9Gb+[6_oCIGpi['_b~;7P=,Od7pmq5Π}|y )1R0lE9)dЄ&J z_]e#H۴ԼӐ6 [ 0U懭bZQ[ڵ32I6mrtǒB}j4)*#<| l*.#=j;Կ'!_'$y&hʝ>3i%.޳䓗0=jSP[i96Ϣ`x"Tvn'n-K/3= V EQNǡ1:Sf##0Rt pےom)>VnAG`;QIG ڿ}Je2~SuRrDZA=}3P~n̓'&zo6J.ˤC&h&v}&^x&8!YGWw[]3>z̠* sH63׈%GZ W؅0x ChzrM('=cļDo3aRߎّd0-n7on>.2@^s8/^~bZ X˫'0WOmRMhnqtܥJ@@TP3!fia {jtܻӺQjòQR1]OB,=n2O,ӡJy,ߖMEt}$^Uv"kGAيL*0EQ%#R<zL;v[VN.~K=_<;-uv˄ Q5d:>TE?(:8:>>ejOqћ6wL}e%x P4εupPȨZ 1n@{b=.w{.W/;,rt0Sziop+[90<1F[/c>s.W;K_w_wSD7b4WqN3{H0U`ZԩH(޶^3 0ifjo$ f3$%j<yW5aE( uEG(qV=D|" .Ew $hju$M `z&0~}="< g!34\${=J2>u[4Dwg Ӂ.h2:qGgHd8y*T`JҚ)0b?\1@Z w+.s|eQBhBh,N4,攪=f[I{&*C.>yۣj972'Q }K'I]6L,U5~[]#n6Ȕ$NR!FL!ՉU:0FKM ]@c0\e\hPZ5\?N07 9Ʉ| ]ʷm5f99A븇{($*i|J*_^S@q}6"p'3] W3:VK;*q(9Sev#kdLzX?l?g<<>C&fzW[ˤ5\qr7Zbݺ76g1`}.߶ ׍,%+ /w44j'P\WfA|vV6&nǗ ;ZٝyB J vl@ J|/J Z9Q& /2ri^9c AZ&xyfxl_шr>0IzDF!V| :f_18)Kx5,2D2彀8D$UDEvw :[Npp-F/Rbf4GVVRcZL$X^ԏ-lrLiȴL<_hr~Ĵ 9#|a.42_fjQ=JW|:HZ֧ 裼>ra֭wXk?>p:щ;97\="sS2յNg#sk>-Rxi XHXė>|&||M+ LQ4gƔGa=E*6]m2-qj^Lw췯z ;,"z6@u wk@?q:]s]oS]ӳV֟*G8mrm2fSKrodҋ04߃}E4I$KWf~[>}[L҈<+!'W_<ڻ7{-E ywJYdҸ3oՓoff?`琿c- Iw7eN[`; ] B8{[ )+R?6i,piIws}L]* m>l /2y w 1yʞX//|dA/=qۆVY](du!:- P㙐6dwXf=6/[O5d`Xt@rd@S{|<EAEbRq[YOq._'O9אS)rskfdWNycd-p մpj!|LP:׃iCxX.DR$へQpj~Y8=@Pyuva1n70<:yD8R5k[hTGHtz-rQ. &b-JI5~|x -[+B+=O֯[u_ W5W cti_R kPv!)z?B9 hujKuf䟋~YSsVGw*t?[ԡ= j]dk< %8dUAeWa-AD22Q~0)Sμ?(:!5*{MnIVgM[ycY=Z ?0.-Je)LRL ҹvqch=a3b~\$X$`p8*TFP]u;ڜ`4C6_vZ/ ъ#GýNBaߪ}rLUȢGgS˔%{}9V㭆bXjD p~Ojh>&->L\չi#'2o{ikǖˡ:$ZFAa}'xR@bPΏLqiLg}m}t>GJ ~)]yHYN4Sӎ1{iHX5ߔV(RP BJM ~g= ZM xG?gXfȭ.lRǼPnkmgHhK`VQD0UPNkZ!0QgiB|| ^bPR%,qp!LQ?ՖԓnTiQq|=G^8u5QP3uiY#lP9եXQ*D2~O${:(t=s-qƮvWLdnsf̔O $/\%`6KBxh{حş53mj'1<53]|l lgxҊI&N^V)aq|!< ;l\ s9l^V:\B$8j]|璼C*c2C1`ZA^iB=GOFlޝzSDuqKQ6{ s q7]WK^-A⑷r5ʖ@ʟ* N%;5fIq$6+%U+:0Pj!2yc?kӏI225dR{q <yf//dLtfUCI&2G&)N*F"5zW 33!=OL~x}@y.Ubv BKӍ=\tFW -KFp*9ҨU;G)Af#tB(OC1T}Ho|OMi7=,8@?xBzKmjڷO-]|RY =_:j:0RǕG;19ª#<GAO^;_[ ҊU‚ɺC=/3ERE/whA0aʶ'1jPu8dT$b<%P.oT4mf*ލ4:W2NZ="RχaTBbq?xT;Ge)| u?*))I&야A§bL*hgAI,r!4%7[+zȩ%~~BIW֣Ox!wϞęB꫽$rKv.{kPq=8ޭa7̄_{Wge O T"i1ąF:r!M֋SD8%jW.3!`hϣPdOɼ$TNpr_Kx8ZQfkB14VpЩk&`>bS[A|C.SQ+fE"t5 ٜKPLG"ܙN9zQAoS &DMUqUj_ 2iZ:oTG7>ѹo$ʌ԰3XYJSoh Wr*b N t|IjJf*3ZOԙ潄)O?dDJh K[o ekRԟr+oJSppM;Il>'Hr$Q{aN{{?)8w|",Qk:߃t \8urxeMl+kgA8/ثk Y{KeqG7Ү+հY౟SQz~|KIIæS@< hd$yneh;5|keQ_pHJcK%UקDB4rYvг]!%kHXDj? sC@7 9)on*yS?'$T@:۹(|˺<.;=snzGD+ϯnoIh$Npd*/wK.$2M푶]Dg7]9s xO78'[j7qxDz d˚]!ƶk 1HzKq;q&qS|2͑ F^ň\ x寿}!V.mn ps| !j( z_PTM{ DT.J>QO(7󾕠F6ڱa[ӳ5B=>(3$;ѽbR͍jzUq~%uYOfCi5>T]0|+JtF骕-8k#t)| NŽ'!v-;/zIC?ސ@AD>{UI7v;,jG3ggRѸu{gݼPݙ2` :uM{[ fKI B1HG~zڅQ(le1PIP1!,cm[mPIR_'܁"ɛ!D6$4F\P;ҪQ D(ժVĤ0._q(NBiuy;-ay4eװީшXļ]_+(\q#-Mǰ"ŧ =-iK/ zU6O>5Wߋ+{U`|VQ[£B h;sa}hC_/hk"tiIk=6 \ ;/)hNoGht)j_n5-Oe8-< Ӯn"KȀzRu*`זݺw_CF#4ўh}z_ϔtd l+tauz`v(&vL)N-o;T|-Smi\KI/rW4d0? !HlF4+S|Af~#˴P݄pN%ɞXq.LzBsš̾[OaB[~J'rM>c,K!и+-CzHncxjc%﫛 /2:jHBZآΩU%d{}kT;JN@W,u@Un{!ZZ7IAqik[R%)VUg¦ 6NJu-J1󄔧'$Ř$P wUatu`o% ʏSsJU>>*s * F]SYl%&p|"#:.twx.׏e_G>h}uD9Bf?YdPR]ܪO.q^:i<&z-lAԸ:-p¼}=b6 =ZW c$g=-a'SM3Pyy2eM/NķDY᎖Z"%X0ljj E=۬6 =^ j1{)%<эZ˜,C7 [-۰# /2}&_է0EeLcmwy{0E3~勛,=R? ?b~M|cW|Ԍ+Ԧ2Op J#?\}L rd^LO P|A2 SχdU6U: #Z"8& z]Rl-G/ϋCLtRJ¾Fe |ˁf*e<} DXN拇'sݵLkEZ1 oA,. /HӉ)Tٜ3Ô$߉-siZ@kKBd=~1V b;J BY6ȎLfO;.po ͈`:|UN)4:y#4nƤ"4!xǎ..R>뷽ڸO`n&DEZ`e%k̨?˶R|bIpvN(iWjsP|`ka _{MR'mVP$n)m!Vl_>▩[n)dEGɕwMC(QFdaf)bW#iZJtx~ 3YA E^VlF; otQE-5 g%C/bqA!VTPS|W|jN<&uerO]KusY|jdm(?Z8\PZǗQ@̎_n|]htCd?6MGPʨ} /l=*du{Hgܡ&~s1gnߠqU.ve.|(J R9;f(sTHIݸoxچ|\Y A?`' k=7xŸ|m)a >^T*HeI7%ɳNIor.znJj2@ ;$t0 bWMJKQz?k,ͣ# F?0V8{KJ**b#K6h6_{5jše*]`ob"qj[LQ wGhXv\j93E\Pcwv4:+騤-Ӳhɉ|s%P H2ƪ|с:̘^$X;hE!( &!`ͪ%ek&(LF 2#xbfV$gVUUwN_j<Z[kը>e%$L6O j@ ˘I9_*:*gj\#~-MỶJ?NDK`W+,*h1m_iND-Vo/ڣ h7KL  $;vX>n&;4$Ґ_jy^<߯flVItT~ ?9Ae1 ^J"ٟ ' EɆ,JG鏅C Ϛ^@C#Pa2To^(Ў ݭD`]u~vζAK*ak!bMx=Pҁ (* %b3T4Y R #q͙b߯؟~Yb7hdރʻ { g'GI5R V* ׸ _{+N)jI(&@JhX cv"fpCǪǐڀ9JJ +an6 EcH(% m-L=ҋ5yWPk(q6ػGzu+/VLy5ƧӪ>Zܛ:͏p5nMiŞ5]ZRfҮJJ$5HC ZnK}E֏n%/eIBIIb*]蜑$sv8塱v_v(8ckbפ^82.J+|1 eM+->] \2s$DQc 4_NS\ۊKe?ި-JM=pjbK&Fxǚ#Z6R:U hYި8G&?QQVW{ȋ-)+JBoWqKOgy kB 3d:QI; vu"Ih xh7FD%vy#-H=,$oX( PA#u0(0]?y(oD.r tRVKNZK%ش"=V&_eu+^ q$ Dňn2=6߇ߑ{97buL_D78eavDҮntU3(f.>7T).Y[̥3"A:^1aEfrFOrŲr6%Fh}ߦ 0#xFڱ)"nnXy={^Dzu3JB[t[L^Rc BҀAWBbj#J|ݸjO`&0u"mNk .8Vqk#!kdw+ t$}^Hea<35*f}#Tlxb/-ll6;rWQw)q'5Tnʇr_'YoËOZBkA Pͫ1CsYD j4~~(~ />ߴ9^'#ar`2LGClh X l- n|aK6sp_-׋}'za*,8\ ܋7czx =7epm Y46>cףf?bj>%ҧ9R@ 7能MsiR) &_ޏI@(>LA@\g2Ѻn;\ QYu0 NCU2K >< @q4X@C0?0s 8|Yw(؅b81SRb_rJS!`'.C9J\۞G:RumcY~c_Xb?A$;7r{C 06 {Hؠ/ʿOIFY2mWҞ7`SQeN³vq$jo27P}~Y6[{c+[|(:qUIpdFM77q.հ1z^ b^49qv/Ty(a{2^P>2j3}L=B5=(zts C ǩ(jMC_Z%@a+2Y-@.MW=эe7tn@"m5~>P)JY*f֛x27]mrb]*!aΈ'2E,nH+ͱɶj7E%wcTQay9W4~JuCQ)LU!9/( 2y݊=Tdb1V0=&"$cYew:*GkAҼ-狲WuJH& H*BKks"oL-K@+=/#tZ-%pu3)m${7GB8xtb?P3~&νe5ۧNi8v{JzAʠN)S嫖FXV?N ,ῤ 2c?. Alo=S߅PF;V4_oI&.=6Nb j95nuiTyr_pi`@׀g":GSau+G}%"!lW+Z*5ڭGka'΢wti5DLn!:%P򨿺 Ym՗Jfr4<\q /2GS^ՖG4NX#fdl@ʫ-zB VL3O KշћPS7<5}$o0M2*U~ *:90E  F3jK,P}!ingr{gji:j4>.?UKJo0jEm(QY'8-]7轑qoy9_Swk<WdG Z* X/B5"`.@NGF^ʢn0a ߱q.5 ;l&QRL`DwG2)БqE[R|[j{*fGŲyf 4t{#xMPux1ƳluqR{%}3Fۧ.ʲqQW=zTyWW3Cqvףb;`*Tv$B1R;_2*F1N/9 ' mR!ԝua~.# K+rHkuX)9PJlqAPP % 8,G8nc$!f˂v3&:8f-=+ecY׽-}&/\"rqY`R82r$qN=[>rٚz0 D܁UݱlELPU\N!On:GF#7.2a77$e\߄UsvZߤky;}[>x3[&,@:xKc?y@$ n B-AiawB^>-~v*0I&A:2 O]2ue_Qj?+:2`7I+!3%^6_7Vi"-ȑG͛O3i+RjӞ%Kg٠v}]ؽyHV \eiq :Us7K bxzS*Ye~:_}qΒZ۫uhjsLSctƻ|f^5yr6ؗ!^$}ja}-7Ti\YzSTT{_@M.f4=?ƏXX`SzEg֬&jR^吝>S@EȊkm I*/;$iYs.?%_{STz9(]a@TQUtn{'9Hj.r~B esM&RF%瑕:qũhd¾pɲ놢Ov|LAB3;xpg4B[ sftMkB a<h{\% t^}$1^|9+ B=+=^@ k$ *\Ø"멈 cuSܣ+A@SAwm׻v ZصAuC"o8vqܮl Lոwx<)S+R4<8n:tȉ@x]nZgטkDrd<'I\;UpdD|PM0{kQ' ~mYnv?1Piy;5|K@,SMBX8MPn:jTq;PȬk\?ҭ7!˿r?  xSi]qe;Z3> ~F?c|Dױw: nm+^őrdbr?ق&+N_8I#d?+ ]5MDqb:aˍ:$5WLd/+CSCtL෿JKNGEbF+h}^ĥT{zvDzn썞܂ 衖lYdTKj4U+쿚f&ߓ~8FD 9{j7 37ܑJ| <[Q=݇n,H86~廾*OOD a,+_#ʊ5k>00ZK$,oh4-tvScдnIKP삋+NNSl)$w|uô rm)Գ\֟8췡<8'h.wW%2B,rMBm.b W@"k/d5(6Ć??yѩz FE}%du*՗~'k8waZWN׫~g!Iqu+܃|]ν]Q,K^RMDXBLXV&$MaǂwNI)¼:ܔ*Sb;" [:ٯ ^,6^n` U >ߛ{Z D!Unz(e8%톀I^U0'zw[,&VfژU1'.V\q-MIRyS4a =okR9%^HVm?mod:og> 2@֕l Ekm a:s~/C z`K5[B=(y8v JyW΍Kd$֍m { =tmfh?9cVgn6l2y^>o3z}7G6@5 =š@OŝظuVwg=ש q*YAs +X9C,t=FhoX-K{fWkyVh=C݊C_Գ1Jgйlb pZ^,q,Fν|,ek?/|E u8/SEIፂP<* lQOV)fiRu9L,xϔG"CjVd-$MHu+|("Kb0.g<-%ۡhdgw5heӕXS t6\z">@MErP1CP/8bd4_!e\G便Pw<+J5i,``/17\M´,襋>tܿ,g]FqvL3;W9-k"|jYRKeceoB|޼)Y.| C:M,@V鏳vWtU#ed_(YUFO:U9 -VyŽG&]:y΍RNFJXhFJ"7yJjkv#&q:P=ΪI{=a0[m1RB՗ vrζ,OP 7Twd5$G;*F,VS Nag oBN X@iCl8֯j_R{Lw60(asIenV>B|V_@Bp> !zxxԿوsf ͜ܟGeUNO鑩E,œ??;e_m<; ,czeT_ kiY˱9D.uB97$n|툅'4~'c4U٠Z#\\=,ϪxKcrߗh`&Xa1qQdV[PΤDZjĹVxv42H %nޝ%VUU\6li5h_}q@(y>^>;J0g%2mj2y!*Xb< gXTqX>ȁ5:L~(dFY<ۡ dUO0/TPP%jr94CۛYϟ'<+q#MTp%&ȱޠ* ^ ;7io(uG:r.3NJwV=,G!T;a*Ag7΋H'\tnZJ(jdqWBaZ',fѵMʩ~̫YZ%0Ix=niMuIΞ-“WnHӧSač ށGj50[.z^c"bᏗqh8mUG[6Stk[7UR-!, lu|9.%@e #~wBO$Yؑ?;*Jd;[Nkg;jgj)~nOc]A*cK!(ir5 ݝUup!۸&lGLg/^CaUMjGO٥x'5دO K Đ8df'ӄ+ gfj8n`+W5m!JAUFjx%%/t .;T΋\T -~3Sm1M7NP9 }m?C&eXDu7=A%Ni!{sPu,D{nT;,k'Za>Щh%sPיGUA#)P+Džk㍂ҳ3Avn7fmy>~e|x&gh+0rEC#TN>%+I(f~/BkHm pmw'Q>ᬕlmf:pa0rYtڝbl{7PQmW±=㸈J z=m`yrnLa<ǹPY!c2h4Ph͗+(} j;˨Y+Lфߏ-J"6c2"l4;GJ֝ q-f5Ä  *J/X}YƝ|)g݂]lc['>JZ=3AYeq5P1L`9l_6)ion!q>߯*l(F;ά|>wp (v_cfhr%Mڿ` 1-PGk; @@ T6Fc%t+dT #oӛ-m1zc5O'Wbҍ.LZn8Nv*G;H%S,,s|(Jds zG`{1_ϋy1=v3[@{Ϧ{OvY8#E=G,ϭ-n-6ac":fD-܏Q9NP-?[5&-|NBXDҚpAp.>~b/vȌ΍o WZbRbH0M(r([[@%nKG w^5Qzk~myi&@l<V  ]^ABIFX};gй4ET\<,_8'kVE9A͞f? 5FQO^$S~=l? } 2j=\h2\ 6D?7:6-E K@,/HQ){^kXZpiFBQiΫ!9Iba])G9JJ0}YW^ L 9Aa9Qlb̷`Z}w;HW6c\WDBiٹ|OOфlĨ?j' /9w:{X8PXDU,1}JO '0ݻ0!b/&Ⰺ?Yd:N,xB7zm-cYULwgk 44 _}_}W C9d tڳd6M [Q pQ>ODav3iD^k+OR$j: 9ׄm{uy'dsxLK\ޓ㳄NioW]2W;ʪ8 jۉwwIWGr0e3yBh[KVJ`G˜liܒY1ֲeMY~ɒWD9B5% @X{} : Kz ၛb#eԀoJْiip[jG1t_aO]ǡw,| (ޒө*-5P䴡bJ&qZ @t.&Kf{ nV =7h@ɟܦˆf)Bg+ި%E{ܣ2R9ibrG B'6CoKb4W'݇ёuޭA<((npn:X5DQ~/3)LP2DpM!/U =s CkXᎨG>IGNG*/hSܣ+j`n* 4 ,R˭ oXIEPpz9!ƨ#R?q2Fs6EPGoXOU&i:'co{ zUsCq oZ<_ſb-YQ迳_s$,ɷ +n oZElՁo>a&fs%Ԓa@< H'S øq"mkwț8%L<ފ*vnwñähD i@RxS|rɓ@qYK>Dy~-sthRA@6=c ?6T V}zNUgILYX9 ؼq H%,.7[K|,O~Xw&'W3QGB.@[ƒ`/Cꃂ1<8ň/< Ib |vbBs$ (mUXjq~`c5vGСZ_}"*AKuV&%2i5c*^8Xcp<0kU{+ӬA"y7/q>Xսl@CNuN9PW 8&~cU *^{ *"]<{t pM}VhFE+[ky80yy3LT[c]jpDZRmU8}Z { &4lhA"r;̗{d[p+y" pJN9 C}_#. N 7[zY=a'UbU4_ s(4;ACcAK{*N6r0PU vyiY&;&Si5n=unF%#n6Kg0.Ou̻te)|4*נUʢyL=+O%Ŏ,Wr`GF?rKu đVKi |,(MNJHT(I 0E72BouîrE]R72OÈ(K4'E*>Vgb!k@^^'g:\Os񐜛H`Z6>|6ܗ$+۱C`l]c,*| a&vk4@ӊYP~׹i|kB[ J_Bw@LT{r0_zBd%B\RÊcƘ"I'(d4AП(JyG9ܨ:FJສ-T$c?B!w=fѥ =.2h_Dü.yK|OjEYf )FE7rR2؉lᔂKkc-ije]Xَ؄8nq+f2=smGbڈbL7Tf:sё^9wg>Б=EԄ OӠh3i~*X'~! A_idEA s;dv2 qle BW/2#Ăi[ͣd[U/gWٮ9sv504OA|ukD$uGrX]^2^ z!L(~+]iv3}%,u-/rh(U;9QB: z< 5:ж}oi]8 Ґk jh_pY\ < bPls:.i*,==+& S,y"nm9խf nkiJ]]to~\J}-VtPjQ&qMfM5yőKAf@C@<x^|E-Us" bڼ@ %j?&8$4a:Nc96 f9+FGZTgc[g~qcN9[цhhuP',uT *Bd/-=M\d6 |[?lfx9ҺTzt<5.A`eWQ5huǹuZmD[r]e(DžmcЌX!f.]< amR(oI; #:ISYIElF+h}U25"@^9xmJ eW%HGL:, w5d믨؎$ „CG1uK;93̣xa61u% &N&4 }z\zCi&wTT\-cIKش.F3x :_'~fq M>JCT1Nwq4<*"(E;iL!⁷ Bi-0u-{@iFIe#N1M.׸BsW},OPK4zD*!";0 n%Xy, m'ov58k hݏZ-~,)z=r=N@Ay3A % cF$ d$SG|U)"~wk ,M$AP{9Y˽>|>, OvވUQpD؈]Rr#6BڷUe*:F|$-.d08j{Qo y-,оv=Ʋb>jwTm;|#Kk9۷Y]_r "oI"X* sY T6F!q3%`){j\emG2rk*v~;! K4?G)n` tȟc}~v380Ym:[%O.8PcMQ\|$A|SdXH3d[2XaU=v}u;xB'8@"LHhs El!T#C(u&NMa(;z{Fa-^},J T6=c _ZKC`VӖI_m _A1fY Pܜ&&ӅK@VP滠dyҺ'(RseP6^ھ.jWJ+&@q[,fpg56$[$ed0 t҈F}C;l VIe)FijR)0g{݈wFCh^ q'A"|?1ECj%*Gܙ@[M)4} Az457<ڃ}e'p (pۗpkZqPRzŽuSWG/17?b^4;"7dmDliT!+gЗc3>ԩxMgE)U&"סE]MdXVŞ?X*9B=s[lUm?,"U{P+\yH[zʱ fmؗPc4󤎪d9-9ul]`%Fb\~+ȗ[7\$x7}23X 'Io!}k=wIIڹqPbѡv5V?f?Q_TJ,]d(.ZAɇ{Z@ԩv\Гd ZHڷT¥HTu}:2wx]QoDp(h bVssLr{]., CzWlIKbī$v\QTv(D2*Vk25O4=cE'tr&TQ@m:eOھ<}|OTJ]Ǣ8\Оhn _H)Ղ *BC-Vϋa"x¿L㼹%$d.Tz+#UvhP#M $e.@;o>8։&D'",~8/!j8q o# :%`UB%ڰ_ vOhj(V2IVplGe5KBD'mM Veԇi˨^ƽem"=eo[7w/ R*kla׾=PaGKz "J#  0 uy4fJnjɷɺ’غ&̧AEkwA0?ҢwAU0r.QZ6T㒥T5Yv5.Dx .rNW9~i(BhegKDWܬM*B-ɐۗS>08jfvf:Qdb+;ӣdl$j㇬q4f>=?wdwB*1K'deSLd{vn3`Q+Z  (J]p8n Β6hlP.y`4 ] fKc^; uE;#S*@_@F?dZ%y)ىN1o_ܱքoN]p!IOR'FG(Q_ 7RZȚ%QdŌQQ_V݃1 }(t4Aap7uPC'amMq6_$pȻ=5I>US0tOT@v1t9x.~;5kyr>#:PS*vpF =QpC )i;G/g+;O) ȥ9S%*hs7r*5x|,m~'a4V̵#'-XHߚ6+r 'W ,л5vVXؤo]S*b)@k:;V+>\OhHjS:p2[9a|We 'oA<2fp x6L#"jrfkhRjfTj&lŸ'8pn,58SΞnlH)&e?&;/)+OQy۴x/gn1F3 { 0e|,nRrZz?R^ۇsZMZXݬExo\K]+|@OsRUj,GTyΧ/~xM}wzƬγ,ܺHҡL"6 VJ[g9s/$ lVCr,UDQ?1)K}ss_,{(rD*F QpoAMx[c E k͐2v YC|\!:Mqdc sȡ;[1AAKEib4u3&㬁\4^o Vq>[:UɏV,h(W|<,jl@=:g-{0n['Eq/{=f;:zMv{[tߝ[eH?! cee|0Q|7__E:A _'l]Ti p)Q(hXoDbh/Zd+5>gX J*2:ߒө*j!Ԅ)::}S:!h@FC#PZ+-'ٗ-p@عћ' .l/H=tl'*祎>h*%]O1, ʝCZ$ӸP'ErnjÄe dC)( ? ;u;(NLgZڗtaOxdɅIN6 :lE9`)l؍,*k8#cM 1=SmRi^m o:m*vה:5 &aO*b6N$'~UX)Fƕ2|`<zC0=%it$H[}rL97up*^?X 5b8}n3 b=_H%$6ݠ2d~ mapхdkZ LC^{s7P)a}).|&q@xaЄ&/;lw3.N\x+DkTa~=RtDmRY2a).o+RNZѧY Ci :<>NSGIq>˛X-CB|Rv;3h^sgY$B̯͢z֦JhlJtU/R4V_K sc]:#\Plr]!:,Db]Y)./p{ϓ\]m3,;WE!R{[@3WLx'ƞ;=i' pgqugl}1*͠Z?NamP-R[+=oa 5Kdލ%#q=:G;0)&əX#:z"\PaQ=:"a*q\=)}NWrXlVbfLo%SI@) 2C72;/2CMHz@ )t$fDC3&?v`e1c+ʖac[ASC䱮ǭ۰;eb ʑH4) C1 $jeჶY6vy[ZƩMCKc*/> pf1 3Â87*LA3Jyll%> 55 (ƺn09F{[kH7v!] n ƣ_܅{i˔EeR)~%; Sĥ]Ttk:+E9zSo^YfA Akf(Mwm#3t>߳;gm\ :}()kz`RnD1c7sv$?A *ha\٭WfnTXVMУhι'v!\^2S}V^a6j6ŝ;mؤ.n 7`I֨s"'wϫC0_A?YaEpkd]HYeY߲ ]RQ`q풺%o,2&ø?ˢ`1,>7NRB6yubOW"e ab7e(J+ Gc:צp™̂ŏbXz|M.}r紪QuP8s_Uwz _*6/"Z\P gpe:oL%FB`4t \]N"pmrǥt 0?3vh#3 td]ASQ\eD谱?p@a Wv?S*^YD^!hᇎ;D-P)_e=X3bhY5f,fܴ`2m-4*M_ZIR)'rȶl5]f}ԩg %ӳ?qqMgӶRXV(v+[$CJf#@0Qf8CiI@1 $xYLk16^ Fﲊ (,udr*{"-LqC X흡Kpd|zCumg0ƲnmaҌ$iټٷ;CYimXzJEED4a iCDzw'LN_%OStvcq4GP,0snM2HnkUwv/ж5KP! 7^?φ`Gx29+ SBDɟYH@M̧H15F0l Jüv YI0.KQ:-(C@sjVDfe<"!VXa A)(I3T  Jm"Xlfr9߃9!4Q6 Lb)MIK$p\ˍnu}Ϫ<}#$J@ Ӻp3Ań@G[`blfa\{;jZ=_A1?onUaJ;Fo8*M}5q$9Hv5x07Ap۸X2:,=t7MjԪ 8⮁y$tM$${h+*˼,p}vE>EdVs*yzaڊE*MD6Vk0JU #<@i[d*\[ڱ i.b $͹r|MZsn z2UilĪ:Hrs?)/-XxӦ;nagV%M`5y<Rqa4 Wsa\'oj ̶z[YU;$ 3u|>>jno:.J/Q: }r)cX*QEF ϋbNf_9p +ɽCm`6^ڗR^_}rK /v iC""gḵ38Q). G9LJ&_J7{ 6q;D[ !]X*!VAU|z`ڽsӤi~w=MsE3";v|ZVboW ]ܾzƵ-A;Na @pBFE͹(1SGф ()XPTL]HPŗK=mҮsCH:hi%u7nI/ezБLjKZpq=ѽ"ɳ2Rn&:5 KR8²O\%o`a+n| w%n ݙKŗz%).bw |aH9.E0~N(Ebq'>yais UDH-Ug±g Jȧm!)u Bm|>V}R~ڟ$i݇]ngDZg&1@DlN w"ۏwb.";`Tτelfi412X ?6Eo.iWR3K?Ƹƫ%RSNșw ;8z|)LH?JC0Ug3,v>`bw3HX(l<4Mw;gQ6#>oV#sTl BX÷_EN> qWI]g]+H\ Q1[Mӣ[&DpNQ* (kSӏHf`9v6*#qZBrXb55;%MGllUP{B9vlwVXIuhS[+x7Vtawϸ!Xc0XBy B#PzDQ>9U%9: dsɟ.Q/hzs'{yOkUOVn˄Qd[*WQyM'')FBi9%g_èvG_0w)̓J;nNmhXĉ  ^v6H>X ukv(B Yqy=״4Roǜm+<)ITw&7 A"l8}]X 6hn.O,Qt#(O=Sv'XO(VӷM7)ASn}i kdf1w􆦾6Oƨ/e_Lg_#6Ȼ|D٩ؾrJ :x*V]B]≪]% •]2^~te|~&h7:i:;'y%&CSmw`-hň~sz(0y10,yΞѐp@E~a&xF;ѮJ#)La{ 2zimGkWQ?D޳8aMIOIdV_Q;ZWo+0 61ͅ}gVE=b+֛?GΤ7QM(`§ &sn BߕVhUG5KDLJHZ+BCY?15D`oeZΦE0=˧Ӝo nNKyIl$DS?G؞!.ߓЫ'?bIa^ hiq>9zkE.Y zʮ6>uC 2rS4ɉzzYĬm)o9} vp8ߦa؛nqkۓ-Ʌ~J=O^ED-~TeJgDƺ^Ѿ-![QY2^U>/ b1"KHY|_̎YHѩ1KC{&{m7ٖƒ InxT]pZ%_Y+٤cK$YF} \?R2,Zvl, !d6ׇٝ3N!}kjJ"Pm}\=K,M6WȲU\K$ :IUڨY,N0$1*k*z["Lm hs|& s(ގn|藴Zb% o9;q^/YV ,jc$<Ne*OBQ2Bzv,j/}d+b`x{^ 5o.>EeR#OMyVP}o %O٫Xy*C/k}eK/(:'1p4KŚ&t3w/i$ԍmUN0ͯp؊,;/bi31TQ4I51j%s^ӣsl笩j%bQ *{`}*djy8 P"gB0#<ߊuZx[\ZU쓰p}@#^c"3y/T.0NeߍDchdV!2gz #v^1Vk?Jw?`hqZyYdɈE|mԾD\D>B}橲S0΄.W8jQ,TznۣuUܮq!$0NÜg#&2oQ.tT)Ygv, NyMޛqb\HU X`[&GhsuK ?Ng[spJQ 4*s>VsTei[n/{Ed?.v*őH8X߃Gpڰ^'WO,MH{7ys`MRyka-b0QzŠRJQ]naE,:К6X/ڊf1S ɦZx?VaV]?NE^l6 Y#ń8s^,oͭ{V7LTɴxRvf2߅hZ ,>YEgh@ D[ia6b<uxtx#8VЌQCj(=eCO/yل~ :܉ g:XJ;TNvP8H=yY,-%S$A|̡{DÝپu u+|Gw/O@%k]d p A\i ~=&>%R;䥸@N>.YhO#m2!./C@"q$]b=ZGKj̱[84t#zM$JIXLңH琻2Q)IBOsz.h@J'U8=>?pmz{VXd>H16,Q|Ae/\A T;#5ya`=曻 ǒx򘈟ɭ*X}_Z&_c8gyfS4| :hcH%I:8[ *)8~|7rjzlA6ɂmqxݲ;pG|jJxe﫹UQ-r:iK.h4!Db9rNN7T(ǰ6KY1wHوaǧ%}Mc_>-{s9ÑBco֐O惐brc d(yl_j%Db~RkKEkt,ocd{{3vpb5</OfY!3IŐ;',l غ ;S#ө~9vtd)*HVEKB-s5]ho֋V (IO|~J㐥X,ϣ÷=>[hذ09"&Q' 'mĕs*Ю'{+aQ6-c@iv ӎ nt!p*8ajZڐSU3VdSI n/=H9i)!AG\-5*11S!ݚ}qʐt>PgFwjُ7_l^֋}o2 }Z~ypJc|K,sX_ԸWzkI5Qt9 ٹgYLǢ)bxEUӽi3RVf / M$r WE,w]H?=p5Uu< j1 ]9ٔo dFd{b斈n$v:% Dʩ3)S|y&sݬz̿mbSv{T4dL|{ _irK C|yz9y7O3q-0QA!'1!&>␍}qREJ;Q9K'}WIn0 zޚ9vZI1fk 0&_:r.OISw@޿F W!C5/?Kfm NloGCq zJuF.'"#A֋5 .a>ӎEP)'+w9 }Rs*l59׫Ɔ?&L:O3!!ïcMB@w_2M\\)ԝ 8Op§n9dV41JSqsXk.|Niˠ'w5#`{5.UX^G q)XhJ)gЍJU6iiGܘE<̋( ծg΋&(>[W?L#gnLy?=lm JmHV8 ^Q0&w  "ReX?U\h"^u2j$߃iL5U{IJ(LEZR/䋰KHuټD9׈ Q2ۈ|B/Y؍Rn-vN2Ss$73mɧz86$ a^+Rxt$]ɑ9wM[g}-VBiGۡ^>S[`SϾItlc',W51OԶk&+C$uA6MTN VR#Й ,xRu/o3(fvc qDeTSxhن&0?O4Z~B31b) $ij- |Qy!B6U| R/06CE5QR+NGKeeFy )jŪIZZKYU!/HA$qBLol _1]>d1r8j8YPH AM|]'t`=r]ֹ{Ɋ$X t='`yMgo8ɏUR#Ң2?i#dQR#Ra$YNQjƩ 4|pn2$`([Mt&{>h>1GSQ# BJ橛]۸J6+Dk{zi ꚲU((zQ_L:Z[1gR,'xӣ@UDTmVsD86Z4Fy% td"pZ+=L: ٗ$W.˘ďht/˨z~`;`IQYUu?/ #3Wvh 窕/$W328WD`Rmm|Hk=aOvB~h ۹da |*iǮSNFvEg(-ӯO|gIi=;} صu6a|[&Xnu\gğ[mium(Eb"ZL/V%=BG^2qB#.{X}K:vO3U$Z}F[[Ͽ [@B`'IffZv Ǫ}j֨ (.I-r#&tL&sNcudZT͔jtl&E؍&Ʊ0SG|$ĨeFpH_e??ٹ<%MViferJ46R 9*\A VwvX3o,.;F0U$N!dK#kt"qra˖5 Xc(<>5|WBXt$ boIH% ٚ5Ei5Y=EF9iWsʥMۂ )FtnU4JZq21/l\8ƈ_*,UGR1~j3n2o:g#ɩ.`!rޗ|" \V *k7t`aY?w#_c{aƆ7^=Rs2q]"?>V';dC*DׂxN22ϡ K^auqovcLpQ{$R=mwI.TA qYS8>FG pAN}t,\vgNWDhc5/{bo KtVIKA8U6VW0]o^A(4`X4k{e-%*8Q3_ )4M,{E_,)}ArYI)z M*"kXPy\XFlD QT@S*?ku4ȘWS)%ԅ'/}H7YR"ua_TOQ~s$^ln48=t+3"ī;@#>"ť3t:cL3(կQ%2ܢ>eš D[$R }.NB pEfw XOčJLM7M{;B318:=;EVdSkT+ٙ Hɔ~zQM0 >ehrn2־;qdY]#\) 4KJ_2rCBPIAJE UdE0[=L[6&tI5=E%2r{FUޫ0 t+&iG )ORüXfkB}[삺;bQ,|Xj8m߄3}p^ 7'^)mЫӲ'6{/ 4GN=Zp*=Mwe {\KO'AYgw: ?w+|qxɷ;U CEM4$_ {}אZ6b#\*#*5_cIW9bg d|lv MsJq7G3)q_(bH 0a:0i%Y&N$]u_~ҵ5>[+W;^R>W~Øj׵ i ^('p])rAP_w)qy *€p'T՞J8t< (h8-.$KD) ]hr$w_rrg?|h0 "BE"[Oubw+c %,a 1lGg4)w3Q<@=r*RJ$jyjV'עO;;Hafb%!I+3b/B-_#pֹy]f n {IJbD z;4֠75zMy&ޕSWKr c6IezΓv{˓:ʑ eO_qGDX}a[ E7Q|O{,U{SEvF*“ q^u dUza~x4$ه ]6oO/tL-6@r9'p=Y R8e vԓbp"{EdEm)pk(䶸>ug$}k`S*8@bUGٱd/ . \>4[Eҿi:~d?iΜu/??BvEXBL^}[LZODe*߶#hjv>\W+$ j?HcmۖQTɁյGI՘h nU6N%<(P]P{! TL`msݝ:X^t6ߓUrʄO T fO* {.xb(/ό{˘T*ՀW'NҜGʬMri*x|-GSc"W}0uA.%*Rb*-IRUG$ܼ>V 3i0zF/OGH-_h>/&Y0m]5y0)4*na?'hPPQ-& 08&@RQ}q}chI՘% 5#(UPj lA>`{%=U_ꁈ7&ӟ)Xu>mquWegٰ/˟,JR9ƟMĀ B^rJX:}kʩ׫`og_V`DY̽yZE$)aRi&6Yօ ;L.j.f<9|B:-O┿Uw!PCiъG0dz"/T(Әhk(\@Jӝ`ou o[&q*}5I6{Ԓ5 Ƕ7JpGCl/ܼdz,b=_+c<>̞={6Jbs*0s}DV}]gJ.1p'|\?$?>Ǵm._{eR5w3l8(%J.*52J__,u8MٔK֡PoYSȯuL|zjB`PL;Ա)o^ZsAs#ة{cqZk_Տ:6nA3eNN')<ɒ&#nXİv$ڭ1*dߣzЗ[FLD ԟlj̈#GJ<@Rz^8bmt-/D!5WjmTu:X4JZ {\lmWZoj4 IŢQAbA8io?Z\/La>$F2<A%8hB҆m@grwj%@\q$8:3(iSu~3Aa;S?8W%\310@\U4n.-[L!Ը>3yGOԷeCvacHp֦];?mg8404G~/kZ*C oCfq|^{L#q0CI"o^5 ֟8$3t6whAl/G*(M] 쮙;i1/9 la*y -썟im^¾p&YqAAGG݆Ŗwz( 4~5LFStH;۞#Di$YJgJՒu-9slM/$<9hKrq}>O{30t9Zu]\y^V~M=~r ::\T2Z¸JKZB8y6.U 3tvXn#@3{Q} Z`b.#: Tc1D7?WE؞<,o.x US("G.j JZȋJbPEƲ1k-'!l@ ѽ3*fF+OMmwbLNc4.&{$iyFbj#˹kٗj)p2l_vCZ&׏V]+)e?*֫Cw0Ok7d|x_RZhhcR2 VIz#dHj;zq"h@~uգWa`I N~0r,Ð`1p]}P@d%e•\ETzXّdN&WYl}]Ƃm7\tXZ&m>Mu"}ɓAs"h)ʿyAO`uVO S!9 I >y&l kҠuʉ;[ ߀3ijW:υզM Dej4կɽ#T^Zl1IUOCjNF"0ލ_2rr&n?0m$;ӎ맫>_&飈7YO5uFP&Z64Vf};hjr QQWJBey/և&BY8̠ wS Vb ~-_{ :z`9S K?ׄt yGR `SϦoVIDMTIև-=Q PADUhNְV:WDpyMaV$ ~+]JOA~uJ>8(`DxzA i*Dy߉wuO=z*iNvrz i䥡ҶlU_zƋ]MlXÿjWԔɟK/TP=QZËژJFyr= >>`"t`Z]{< |>Wf.(jzҤu$8_"4[5pM1}B.N']JYC*d[.dStŋ>2_?W$%({kD50c]^#L5P+@nObYn8axdgg p@BͰ^:bVdL#xEiY!RKQ&tBjGB vL=DTO-r~A< sf߅XZ⹼f$}Efbwpp2=؀g$j1]azy l+a! \ 2/j LrY/,%y^}wTzpCV.*Ћt)ʬn_s&)'lK|ϧ8갵VX ݛW÷ߑ׈41 P5B[1!:޳6¹NxMW/ wh<Cٳf)KO-Rr fWkn%pp˜hmnK O5**0;^nw@Ɵ2ZՔJX CM4e "dcXT׳f|z2G g,P`l{+*$9E,bbG35@1kw ܚc5hbDpwj_]GZYLxCi c&> P1˞n12jOӸE'{&џeH(DƯG_Dl);e]WG7idO߼6m~J^<+ ^nꗯXMbjG\.5%,%`Ј#ڲX=;O.}xa 2wXtE/™6[*]?uk?g0= tς v^H$RCᜯξ\CMm4-s' @s;*JH&LX$f"(^d8_E)}^0} :ko|+{D !YhbqoPpL~d;ysu0HZ4g\A :A_x4O"W<_| ь+\lZiSWUPA~B7kM̌KAAD_*;^QY/BOѝEǣֱ8݉շR}4!S7x,JjnNh53k'Y]8ȺSΈC>sŞE~Έ? I5+Qf7zvLtװX uh` l@(QiGZ.fKR^5\Up4 ;֛fB#.#SA$FsdyQJbJmGՉXeQu5Z'DfC۪{LMTPb`Ж|IbY|J Z (xn{f`2g rPZ"$pQ9-Ԧ|*:9abl閛ҢM[$$\Gugz? ?,\h![ Gus`ϴh,hL"^JNJ6BU_Q5u} ^$t5j~t'Wh|oV+^ v8<<tҺO!:(f P@?8IvM# kEAa(WCq4A(zi”ab ^ȧENiFO~wݚN"6S#?U z mAeҘ5T?O0ӷaF@~p=#7ST(iPg/p*8R-ބЯzet:Zzi"; 1`9< vXxHzx|?Z!H릦 (M blAO`cÁ]uv&9oKwImQ 7oa?3cne5侨p~q:truw˅b4irpo}CBe(kEն웄m)|Ky\4tJ%BGj8׋kfx yC NW0S"Wb-i_EE^·h좭0G?#& M5 .؝jd9)ч&ɚ&O b 'ZY|O@f4:l%Vl!B6fgc((%vzVtt PDEHx j#x9)NC>-5d(\XP/6kgFT|hCҮn]/jZBdHy5ĝV<Ȣccw KGxCagFv΀̺WBr޺ac}O>YZc(q /KfDMWRs*myb[ X:&^FnGwJQ&HFT|(N 8#BiVucos2FS1+Ĩf 星L\&Ze/59j=Pj^{Kֈg;rV↔?`bTvknA\b@jw例x$12 t+IUuVgGٯ[b~=gO'?ҥA24GSaZM\c>a˫mI՟mD~CF3P!g&=Şg uy:4snL:زrH#qcc *eHgARGkȃN~wѼl3^!:,9 kG+7kD%4vuu7L|W<*N%1_{{"-1֚-hdgUQ`4"Y|Nv|Zl[h-ӣT‹Cf- ogJUAtpkՃٻjɟ[T'Nу_By^6D7:&O˧Ŵ^hLhglEi+8`EuBhP$XhxBo_VV(8?~ovfRwD)${Y+㤊ޙ)SBvÿ ?~&H@ *'z̖`l@cU :cЦeE/,2i=xIH=gV=E.r2&G̵5Dv:?]oC{>8zYcDOdOk:8%$JN8F-R}.Q5gpdy"LDIq׾Exy_=H3RdJs`>gtehS7?P(W6iyJBd_hdthU񤀂^q ض8.P" I /$p/_R ǫ%` AlGjϧ弋: ;AB{TƘr;kT^yZ?X음]=rNMC%)KTowv;f?=Ee#pObN='(P p/ YARvs,-gc6{7BqzZl1vI0^6+Wqn1«FLI\C5>_T*ǞYQM6ve$l',)U+ uM֧+e h44Y=>w[ذhč853«miv(는;Hd ٥ @4_uA*-%ӤX#VO&K4$VSUea S8cJiS|Ҫȏ-~7N}efs%Jϭ\$mkj&O <:4Vw볩 mD:P3-MIuws|kkrw/)@' Tةh=HdrX$z y2'KkyI,/#EΚ̻ހ6' ɒ%ū `PG}o޾)o2B {G}QP*Eת.ߑt T+[tf.7sİγS h a3nA)fTJxo?B0AfṮ~f``.ó/SByDG0c&/(0/x\-n/ ,*FlšnO|7ɚtJpMį.S+!hPYCZ\wBMf":H?АGMNv6+O'xQQA 'yP1KN? IT׏ճX4*_Q g~sҗF1qS#&Ty erΊ#kt) ^:AѸ3xRTNz^Z?;4Fwyqǡύ) /L+4;!a}xkU"$ɱ> _Bd/,ns1_#F]$CҴ,0M'?hg-lbOٷzA A7ǔ"eW7MQ?ud ~hj:am:>dLzG x^^pJTܔP3$7j1mt=jkě/U'kQ"9ߒl9DuXY곉~6muEy:Z$ g~(ٻTaJPYg,gLkǑx#=DU+S\ӃCF(A⾅v99kZJ̏N`[IX;T)ӎ uP?+*Y2.`j:x7@/1B >NJ'ԇ~ۢn M؄_NUdkT}VX/7#p{{5hY H^7 /wE]`, Ua<05;LKD=)l\8 QEXث=Di`i3.ؑA+!՟˶$ Un&&=Z+ؔi)9 ď".nMy!, !P/PlٔtThP͜ICmGi{ '*F5&_XMա G:u7`%:Zt<2ZWHkﰒ_sdުN?MNHxݠN2کѬC1ry>l g~!$hpS~vIAKiBZ.~,f.8/@Dz.QrDp㎺[A}Dr |q$؂_{d"U,_Cgï9#3 i?6``TeX73h˯G.;,ls:K~!Q Czw'{d vp=V7Y)n me=_7i}S$)J\Ҕt^'-DQVޘJ:x t7\Mun ׹/G/lBb7l};}zYج$g=9]=1l@w?΀8=vU{īV%[ UVЄBrZ Gg&w:ݻtczURH᝽ŸIq>2\TBs)3Sv`4;Px0.z 79\ '/7i&UF!`OݹJ\׬S|N{zɐӼo' :X;tNn9]ux˞qŷ~O^pȋ&<ʼ7˜m uy5Ar}tB}x ~`;7k!h`î6-I$j5< ECP&K.c%uꨁlGixAwsSomJym@P=ɢ5G\ KUYu6Jd>oFuǍD (I!`;8`Y Rc2wѵ|OONtLF:&֯iy:nCY&[AEDe ^OǴVz.._ GS\Jx(GH}&v{잦قa6m֍׮pƦ-Jh*:(%i"AЧрZ"]O@"yӲ!M^A^BeOVoCH(8K֦<ܟrO]Ġ _G$ GT>ObHZt|υ;h@}L"h]4˜XW?rG^w^~(1u5|ξ )˾B iIJesآh>3[~)yG@n7?lP^;̇ʈY~]Ni:lC`Am,f0Ƨv~LOC}!F@yuq9}nH/z[^y!eH"e ZFVf&igݪi[foƍUFHԆYi3_ u6I!mPU+OڣA[$#S0߉r`3x^a?GڨFNf:VrE3`Sϊ 2+am0c`IM+{zt"rjOrFPR)W} 7DWuR55il(:N(n6 C&~;r,*BaҨ8*| QydI9EE}l,K<d2OKT6Yk3&΢ 7mcǴ6f' oCF{D靝*eQd'epeԺlœU39IfzbѨiԡ+Idɂ( nhCCT _p c2z(k-I̺NjQjzڅ(-.nuLb1ĈqF!^Nх_ns6+߯~]ԾysƗ3H\qmyMfm6O+3crҌx  'HxY<[poɦW_lW戶L1qڞȖG\N~:K&sU/$bT/ݱv-Z>pPmZrJHtMJRvuTy%mRK7R6|h('nU9MBuS` k; xac+*:]JdQG^ɭ բ9/JD/!]/3slixmpp/docs/requirements.txt000066400000000000000000000000601477105560000167670ustar00rootroot00000000000000sphinx-autodoc-typehints typing_extensions furo slixmpp/docs/sleekxmpp.rst000066400000000000000000000001561477105560000162530ustar00rootroot00000000000000Coming from SleekXMPP --------------------- .. toctree:: :maxdepth: 2 differences using_asyncio slixmpp/docs/using_asyncio.rst000066400000000000000000000123721477105560000171200ustar00rootroot00000000000000.. _using_asyncio: ============= Using asyncio ============= Block on IQ sending ~~~~~~~~~~~~~~~~~~~ :meth:`.Iq.send` now returns a :class:`~.Future` so you can easily block with: .. code-block:: python result = await iq.send() .. warning:: If the reply is an IQ with an ``error`` type, this will raise an :class:`.IqError`, and if it timeouts, it will raise an :class:`.IqTimeout`. Don't forget to catch it. You can still use callbacks instead. XEP plugin integration ~~~~~~~~~~~~~~~~~~~~~~ The same changes from the SleekXMPP API apply, so you can do: .. code-block:: python iq_info = await self.xmpp['xep_0030'].get_info(jid) Callbacks, Event Handlers, and Stream Handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IQ callbacks and :term:`Event Handlers ` can be coroutine functions; in this case, they will be scheduled in the event loop using :meth:`.asyncio.ensure_future` and not ran immediately. A :class:`.CoroutineCallback` class has been added as well for :term:`Stream Handlers `, which will use :meth:`.asyncio.async` to schedule the callback. Running the event loop ~~~~~~~~~~~~~~~~~~~~~~ You can handle the event loop in any way you like, either forever, until an event, only for a specific duration, in conjonction with another asyncio user, anything goes. But remember slixmpp will only process events and send messages when its event loop is running. Using connect() ~~~~~~~~~~~~~~~ :meth:`.XMLStream.connect` schedules a lot of things in the background, but that only holds true if the event loop is running! That is why in all examples we usually call connect() right before calling a `loop.run_…` function. Using a different event loop ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Immediately upon XMPP object creation (`ClientXMPP` / `ComponentXMPP`) you should sets its `loop` attribute to whatever you want, and ideally this should work. This path is less tested, so it may break, if that is the case please report a bug. Any access to the `loop` attribute if not user-initialized will set it to the default asyncio event loop by default. .. warning:: If the loop attribute is modified at runtime, the application will probably end up in an hybrid state and asyncio may complain loudly that things bound to an event loop are being ran in another. Try to avoid that situation. Examples ~~~~~~~~ Blocking until the session is established ----------------------------------------- This code blocks until the XMPP session is fully established, which can be useful to make sure external events aren’t triggering XMPP callbacks while everything is not ready. .. code-block:: python import asyncio, slixmpp client = slixmpp.ClientXMPP('jid@example', 'password') client.connected_event = asyncio.Event() callback = lambda _: client.connected_event.set() client.add_event_handler('session_start', callback) client.connect() loop = asyncio.get_event_loop() loop.run_until_complete(event.wait()) # do some other stuff before running the event loop, e.g. # loop.run_until_complete(httpserver.init()) asyncio.get_event_loop().run_forever() Use with other asyncio-based libraries -------------------------------------- This code interfaces with aiohttp to retrieve two pages asynchronously when the session is established, and then send the HTML content inside a simple . .. code-block:: python import aiohttp, slixmpp async def get_pythonorg(event): async with aiohttp.ClientSession() as session: async with session.get('http://www.python.org') as resp: text = await req.text() client.send_message(mto='jid2@example', mbody=text) async def get_asyncioorg(event): async with aiohttp.ClientSession() as session: async with session.get('http://www.asyncio.org') as resp: text = await req.text() client.send_message(mto='jid3@example', mbody=text) client = slixmpp.ClientXMPP('jid@example', 'password') client.add_event_handler('session_start', get_pythonorg) client.add_event_handler('session_start', get_asyncioorg) client.connect() client.loop.run_until_complete(client.disconnected) Blocking Iq ----------- This client checks (via XEP-0092) the software used by every entity it receives a message from. After this, it sends a message to a specific JID indicating its findings. .. code-block:: python import asyncio, slixmpp class ExampleClient(slixmpp.ClientXMPP): def __init__(self, *args, **kwargs): slixmpp.ClientXMPP.__init__(self, *args, **kwargs) self.register_plugin('xep_0092') self.add_event_handler('message', self.on_message) async def on_message(self, event): # You should probably handle IqError and IqTimeout exceptions here # but this is an example. version = await self['xep_0092'].get_version(message['from']) text = "%s sent me a message, he runs %s" % (message['from'], version['software_version']['name']) self.send_message(mto='master@example.tld', mbody=text) client = ExampleClient('jid@example', 'password') client.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/000077500000000000000000000000001477105560000143755ustar00rootroot00000000000000slixmpp/examples/IoT_TestDevice.py000077500000000000000000000146231477105560000175720ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Implementation of xeps for Internet of Things # http://wiki.xmpp.org/web/Tech_pages/IoT_systems # Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from os.path import basename, join as pjoin from argparse import ArgumentParser from urllib import urlopen from getpass import getpass import asyncio import slixmpp from slixmpp.plugins.xep_0323.device import Device #from slixmpp.exceptions import IqError, IqTimeout class IoT_TestDevice(slixmpp.ClientXMPP): """ A simple IoT device that can act as server or client """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.session_start) self.add_event_handler("message", self.message) self.device=None self.releaseMe=False self.beServer=True self.clientJID=None def datacallback(self,from_jid,result,nodeId=None,timestamp=None,fields=None,error_msg=None): """ This method will be called when you ask another IoT device for data with the xep_0323 se script below for the registration of the callback """ logging.debug("we got data %s from %s",str(result),from_jid) def beClientOrServer(self,server=True,clientJID=None ): if server: self.beServer=True self.clientJID=None else: self.beServer=False self.clientJID=clientJID def testForRelease(self): # todo thread safe return self.releaseMe def doReleaseMe(self): # todo thread safe self.releaseMe=True def addDevice(self, device): self.device=device def session_start(self, event): self.send_presence() self.get_roster() # tell your preffered friend that you are alive self.send_message(mto='jocke@jabber.sust.se', mbody=self.boundjid.bare +' is now online use xep_323 stanza to talk to me') if not(self.beServer): session=self['xep_0323'].request_data(self.boundjid.full,self.clientJID,self.datacallback) def message(self, msg): if msg['type'] in ('chat', 'normal'): logging.debug("got normal chat message" + str(msg)) ip=urlopen('http://icanhazip.com').read() msg.reply("Hi I am " + self.boundjid.full + " and I am on IP " + ip).send() else: logging.debug("got unknown message type %s", str(msg['type'])) class TheDevice(Device): """ This is the actual device object that you will use to get information from your real hardware You will be called in the refresh method when someone is requesting information from you """ def __init__(self,nodeId): Device.__init__(self,nodeId) self.counter=0 def refresh(self,fields): """ the implementation of the refresh method """ self._set_momentary_timestamp(self._get_timestamp()) self.counter+=self.counter self._add_field_momentary_data(self, "Temperature", self.counter) if __name__ == '__main__': # Setup the command line arguments. # # This script can act both as # "server" an IoT device that can provide sensorinformation # python IoT_TestDevice.py -j "serverjid@yourdomain.com" -p "password" -n "TestIoT" --debug # # "client" an IoT device or other party that would like to get data from another device parser = ArgumentParser() # Output verbosity options. 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("-t", "--pingto", help="set jid to ping", action="store", type="string", dest="pingjid", default=None) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") # IoT test parser.add_argument("-c", "--sensorjid", dest="sensorjid", help="Another device to call for data on", default=None) parser.add_argument("-n", "--nodeid", dest="nodeid", help="I am a device get ready to be called", default=None) args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = IoT_TestDevice(args.jid,args.password) xmpp.register_plugin('xep_0030') #xmpp['xep_0030'].add_feature(feature='urn:xmpp:iot:sensordata', # node=None, # jid=None) xmpp.register_plugin('xep_0323') xmpp.register_plugin('xep_0325') if args.nodeid: # xmpp['xep_0030'].add_feature(feature='urn:xmpp:sn', # node=args.nodeid, # jid=xmpp.boundjid.full) myDevice = TheDevice(args.nodeid); # myDevice._add_field(name="Relay", typename="numeric", unit="Bool"); myDevice._add_field(name="Temperature", typename="numeric", unit="C") myDevice._set_momentary_timestamp("2013-03-07T16:24:30") myDevice._add_field_momentary_data("Temperature", "23.4", flags={"automaticReadout": "true"}) xmpp['xep_0323'].register_node(nodeId=args.nodeid, device=myDevice, commTimeout=10); xmpp.beClientOrServer(server=True) while not(xmpp.testForRelease()): xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) logging.debug("lost connection") if args.sensorjid: logging.debug("will try to call another device for data") xmpp.beClientOrServer(server=False,clientJID=args.sensorjid) xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) logging.debug("ready ending") else: print("noopp didn't happen") slixmpp/examples/adhoc_provider.py000077500000000000000000000142561477105560000177520ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class CommandBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that provides a basic adhoc command. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() # We add the command after session_start has fired # to ensure that the correct full JID is used. # If using a component, may also pass jid keyword parameter. self['xep_0050'].add_command(node='greeting', name='Greeting', handler=self._handle_command) def _handle_command(self, iq, session): """ Respond to the initial request for a command. Arguments: iq -- The iq stanza containing the command request. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ form = self['xep_0004'].make_form('form', 'Greeting') form['instructions'] = 'Send a custom greeting to a JID' form.addField(var='greeting', ftype='text-single', label='Your greeting') session['payload'] = form session['next'] = self._handle_command_complete session['has_next'] = False # Other useful session values: # session['to'] -- The JID that received the # command request. # session['from'] -- The JID that sent the # command request. # session['has_next'] = True -- There are more steps to complete # session['allow_complete'] = True -- Allow user to finish immediately # and possibly skip steps # session['cancel'] = handler -- Assign a handler for if the user # cancels the command. # session['notes'] = [ -- Add informative notes about the # ('info', 'Info message'), command's results. # ('warning', 'Warning message'), # ('error', 'Error message')] return session def _handle_command_complete(self, payload, session): """ Process a command result from the user. Arguments: payload -- Either a single item, such as a form, or a list of items or forms if more than one form was provided to the user. The payload may be any stanza, such as jabber:x:oob for out of band data, or jabber:x:data for typical data forms. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ # In this case (as is typical), the payload is a form form = payload greeting = form['values']['greeting'] self.send_message(mto=session['from'], mbody="%s, World!" % greeting, mtype='chat') # Having no return statement is the same as unsetting the 'payload' # and 'next' session values and returning the session. # Unless it is the final step, always return the session dictionary. session['payload'] = None session['next'] = None return session if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = CommandBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency':15}) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/adhoc_user.py000077500000000000000000000140341477105560000170700ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class CommandUserBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that uses the adhoc command provided by the adhoc_provider.py example. """ def __init__(self, jid, password, other, greeting): slixmpp.ClientXMPP.__init__(self, jid, password) self.command_provider = other self.greeting = greeting # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) self.add_event_handler("message", self.message) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() # We first create a session dictionary containing: # 'next' -- the handler to execute on a successful response # 'error' -- the handler to execute if an error occurs # The session may also contain custom data. session = {'greeting': self.greeting, 'next': self._command_start, 'error': self._command_error} self['xep_0050'].start_command(jid=self.command_provider, node='greeting', session=session) def message(self, msg): """ Process incoming message stanzas. Arguments: msg -- The received message stanza. """ logging.info(msg['body']) def _command_start(self, iq, session): """ Process the initial command result. Arguments: iq -- The iq stanza containing the command result. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ # The greeting command provides a form with a single field: # # # form = self['xep_0004'].make_form(ftype='submit') form.addField(var='greeting', value=session['greeting']) session['payload'] = form # We don't need to process the next result. session['next'] = None # Other options include using: # continue_command() -- Continue to the next step in the workflow # cancel_command() -- Stop command execution. self['xep_0050'].complete_command(session) def _command_error(self, iq, session): """ Process an error that occurs during command execution. Arguments: iq -- The iq stanza containing the error. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ logging.error("COMMAND: %s %s" % (iq['error']['condition'], iq['error']['text'])) # Terminate the command's execution and clear its session. # The session will automatically be cleared if no error # handler is provided. self['xep_0050'].terminate_command(session) self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-o", "--other", dest="other", help="JID providing commands") parser.add_argument("-g", "--greeting", dest="greeting", help="Greeting") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.other is None: args.other = input("JID Providing Commands: ") if args.greeting is None: args.greeting = input("Greeting: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = CommandUserBot(args.jid, args.password, args.other, args.greeting) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/admin_commands.py000077500000000000000000000114241477105560000177250ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class AdminCommands(slixmpp.ClientXMPP): """ A simple Slixmpp bot that uses admin commands to add a new user to a server. """ def __init__(self, jid, password, command): slixmpp.ClientXMPP.__init__(self, jid, password) self.command = command self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() def command_success(iq, session): print('Command completed') if iq['command']['form']: for var, field in iq['command']['form']['fields'].items(): print('%s: %s' % (var, field['value'])) if iq['command']['notes']: print('Command Notes:') for note in iq['command']['notes']: print('%s: %s' % note) self.disconnect() def command_error(iq, session): print('Error completing command') print('%s: %s' % (iq['error']['condition'], iq['error']['text'])) self['xep_0050'].terminate_command(session) self.disconnect() def process_form(iq, session): form = iq['command']['form'] answers = {} for var, field in form['fields'].items(): if var != 'FORM_TYPE': if field['type'] == 'boolean': answers[var] = input('%s (y/n): ' % field['label']) if answers[var].lower() in ('1', 'true', 'y', 'yes'): answers[var] = '1' else: answers[var] = '0' else: answers[var] = input('%s: ' % field['label']) else: answers['FORM_TYPE'] = field['value'] form['type'] = 'submit' form['values'] = answers session['next'] = command_success session['payload'] = form self['xep_0050'].complete_command(session) session = {'next': process_form, 'error': command_error} command = self.command.replace('-', '_') handler = getattr(self['xep_0133'], command, None) if handler: handler(session={ 'next': process_form, 'error': command_error }) else: print('Invalid command name: %s' % self.command) self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-c", "--command", dest="command", help="admin command to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.command is None: args.command = input("Admin command: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = AdminCommands(args.jid, args.password, args.command) xmpp.register_plugin('xep_0133') # Service Administration # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/confirm_answer.py000077500000000000000000000057571477105560000200040ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError log = logging.getLogger(__name__) class AnswerConfirm(slixmpp.ClientXMPP): """ A basic client demonstrating how to confirm or deny an HTTP request. """ def __init__(self, jid, password, trusted): slixmpp.ClientXMPP.__init__(self, jid, password) self.add_event_handler("http_confirm", self.confirm) self.add_event_handler("session_start", self.start) def start(self, *args): self.make_presence().send() def prompt(self, stanza): confirm = stanza['confirm'] print('Received confirm request %s from %s to access %s using ' 'method %s' % ( confirm['id'], stanza['from'], confirm['url'], confirm['method']) ) result = input("Do you accept (y/N)? ") return 'y' == result.lower() def confirm(self, stanza): if self.prompt(stanza): reply = stanza.reply() else: reply = stanza.reply() reply.enable('error') reply['error']['type'] = 'auth' reply['error']['code'] = '401' reply['error']['condition'] = 'not-authorized' reply.append(stanza['confirm']) reply.send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") # Other options. parser.add_argument("-t", "--trusted", nargs='*', help="List of trusted JIDs") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = AnswerConfirm(args.jid, args.password, args.trusted) xmpp.register_plugin('xep_0070') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/confirm_ask.py000077500000000000000000000102021477105560000172400ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. import sys import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError, IqError from slixmpp import asyncio log = logging.getLogger(__name__) class AskConfirm(slixmpp.ClientXMPP): """ A basic client asking an entity if they confirm the access to an HTTP URL. """ def __init__(self, jid, password, recipient, id, url, method): slixmpp.ClientXMPP.__init__(self, jid, password) self.recipient = recipient self.id = id self.url = url self.method = method # Will be used to set the proper exit code. self.confirmed = asyncio.Future() self.add_event_handler("session_start", self.start) self.add_event_handler("message", self.start) self.add_event_handler("http_confirm_message", self.confirm) def confirm(self, message): print(message) if message['confirm']['id'] == self.id: if message['type'] == 'error': self.confirmed.set_result(False) else: self.confirmed.set_result(True) async def start(self, event): log.info('Sending confirm request %s to %s who wants to access %s using ' 'method %s...' % (self.id, self.recipient, self.url, self.method)) try: confirmed = await self['xep_0070'].ask_confirm(self.recipient, id=self.id, url=self.url, method=self.method, message='Plz say yes or no for {method} {url} ({id}).') if isinstance(confirmed, slixmpp.Message): confirmed = await self.confirmed else: confirmed = True except IqError: confirmed = False if confirmed: print('Confirmed') else: print('Denied') self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") # Other options. parser.add_argument("-r", "--recipient", required=True, help="Recipient JID") parser.add_argument("-i", "--id", required=True, help="id TODO") parser.add_argument("-u", "--url", required=True, help="URL the user tried to access") parser.add_argument("-m", "--method", required=True, help="HTTP method used") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = AskConfirm(args.jid, args.password, args.recipient, args.id, args.url, args.method) xmpp.register_plugin('xep_0070') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) sys.exit(0 if xmpp.confirmed else 1) slixmpp/examples/custom_stanzas/000077500000000000000000000000001477105560000174525ustar00rootroot00000000000000slixmpp/examples/custom_stanzas/custom_stanza_provider.py000077500000000000000000000106321477105560000246350ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp import ClientXMPP, Iq from slixmpp.exceptions import IqError, IqTimeout, XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from stanza import Action class ActionBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that receives a custom stanza from another client. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) self.register_handler( Callback('Some custom iq', StanzaPath('iq@type=set/action'), self._handle_action)) self.add_event_handler('custom_action', self._handle_action_event) register_stanza_plugin(Iq, Action) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() self.get_roster() def _handle_action(self, iq): """ Raise an event for the stanza so that it can be processed in its own thread without blocking the main stanza processing loop. """ self.event('custom_action', iq) async def _handle_action_event(self, iq): """ Respond to the custom action event. """ method = iq['action']['method'] param = iq['action']['param'] if method == 'is_prime' and param == '2': print("got message: %s" % iq) rep = iq.reply() rep['action']['status'] = 'done' await rep.send() elif method == 'bye': print("got message: %s" % iq) rep = iq.reply() rep['action']['status'] = 'done' await rep.send() self.disconnect() else: print("got message: %s" % iq) rep = iq.reply() rep['action']['status'] = 'error' await rep.send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = ActionBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency':15}) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/custom_stanzas/custom_stanza_user.py000077500000000000000000000105511477105560000237610ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from stanza import Action class ActionUserBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that sends a custom action stanza to another client. """ def __init__(self, jid, password, other): slixmpp.ClientXMPP.__init__(self, jid, password) self.action_provider = other # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) self.add_event_handler("message", self.message) register_stanza_plugin(Iq, Action) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() await self.send_custom_iq() async def send_custom_iq(self): """Create and send two custom actions. If the first action was successful, then send a shutdown command and then disconnect. """ iq = self.Iq() iq['to'] = self.action_provider iq['type'] = 'set' iq['action']['method'] = 'is_prime' iq['action']['param'] = '2' try: resp = await iq.send() if resp['action']['status'] == 'done': #sending bye iq2 = self.Iq() iq2['to'] = self.action_provider iq2['type'] = 'set' iq2['action']['method'] = 'bye' await iq2.send() self.disconnect() except XMPPError: print('There was an error sending the custom action.') def message(self, msg): """ Process incoming message stanzas. Arguments: msg -- The received message stanza. """ logging.info(msg['body']) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-o", "--other", dest="other", help="JID providing custom stanza") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.other is None: args.other = input("JID Providing custom stanza: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = ActionUserBot(args.jid, args.password, args.other) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/custom_stanzas/stanza.py000066400000000000000000000035311477105560000213260ustar00rootroot00000000000000from slixmpp.xmlstream import ElementBase class Action(ElementBase): """ A stanza class for XML content of the form: X X X """ #: The `name` field refers to the basic XML tag name of the #: stanza. Here, the tag name will be 'action'. name = 'action' #: The namespace of the main XML tag. namespace = 'slixmpp:custom:actions' #: The `plugin_attrib` value is the name that can be used #: with a parent stanza to access this stanza. For example #: from an Iq stanza object, accessing: #: #: iq['action'] #: #: would reference an Action object, and will even create #: an Action object and append it to the Iq stanza if #: one doesn't already exist. plugin_attrib = 'action' #: Stanza objects expose dictionary-like interfaces for #: accessing and manipulating substanzas and other values. #: The set of interfaces defined here are the names of #: these dictionary-like interfaces provided by this stanza #: type. For example, an Action stanza object can use: #: #: action['method'] = 'foo' #: print(action['param']) #: del action['status'] #: #: to set, get, or remove its values. interfaces = {'method', 'param', 'status'} #: By default, values in the `interfaces` set are mapped to #: attribute values. This can be changed such that an interface #: maps to a subelement's text value by adding interfaces to #: the sub_interfaces set. For example, here all interfaces #: are marked as sub_interfaces, and so the XML produced will #: look like: #: #: #: foo #: sub_interfaces = interfaces slixmpp/examples/disco_browser.py000077500000000000000000000131611477105560000176200ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import IqError, IqTimeout class Disco(slixmpp.ClientXMPP): """ A demonstration for using basic service discovery. Send a disco#info and disco#items request to a JID/node combination, and print out the results. May also request only particular info categories such as just features, or just items. """ def __init__(self, jid, password, target_jid, target_node='', get=''): slixmpp.ClientXMPP.__init__(self, jid, password) # Using service discovery requires the XEP-0030 plugin. self.register_plugin('xep_0030') self.get = get self.target_jid = target_jid self.target_node = target_node # Values to control which disco entities are reported self.info_types = ['', 'all', 'info', 'identities', 'features'] self.identity_types = ['', 'all', 'info', 'identities'] self.feature_types = ['', 'all', 'info', 'features'] self.items_types = ['', 'all', 'items'] # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. In this case, we send disco#info and disco#items stanzas to the requested JID and print the results. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ await self.get_roster() self.send_presence() try: if self.get in self.info_types: # function using the callback parameter. info = await self['xep_0030'].get_info(jid=self.target_jid, node=self.target_node) if self.get in self.items_types: # The same applies from above. Listen for the # disco_items event or pass a callback function # if you need to process a non-blocking request. items = await self['xep_0030'].get_items(jid=self.target_jid, node=self.target_node) if self.get not in self.info_types and self.get not in self.items_types: logging.error("Invalid disco request type.") return except IqError as e: logging.error("Entity returned an error: %s" % e.iq['error']['condition']) except IqTimeout: logging.error("No response received.") else: header = 'XMPP Service Discovery: %s' % self.target_jid print(header) print('-' * len(header)) if self.target_node != '': print('Node: %s' % self.target_node) print('-' * len(header)) if self.get in self.identity_types: print('Identities:') for identity in info['disco_info']['identities']: print(' - %s' % str(identity)) if self.get in self.feature_types: print('Features:') for feature in info['disco_info']['features']: print(' - %s' % feature) if self.get in self.items_types: print('Items:') for item in items['disco_items']['items']: print(' - %s' % str(item)) finally: self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=Disco.__doc__) parser.add_argument("-q","--quiet", help="set logging to ERROR", action="store_const", dest="loglevel", const=logging.ERROR, default=logging.ERROR) parser.add_argument("-d","--debug", help="set logging to DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.ERROR) # JID and password options. 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("query", choices=["all", "info", "items", "identities", "features"]) parser.add_argument("target_jid") parser.add_argument("node", nargs='?') args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the Disco browser. xmpp = Disco(args.jid, args.password, args.target_jid, args.node, args.query) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/download_avatars.py000077500000000000000000000126271477105560000203120ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError from slixmpp import asyncio FILE_TYPES = { 'image/png': 'png', 'image/gif': 'gif', 'image/jpeg': 'jpg' } class AvatarDownloader(slixmpp.ClientXMPP): """ A basic script for downloading the avatars for a user's contacts. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.start) self.add_event_handler("changed_status", self.wait_for_presences) self.add_event_handler('vcard_avatar_update', self.on_vcard_avatar) self.add_event_handler('avatar_metadata_publish', self.on_avatar) self.received = set() self.presences_received = asyncio.Event() self.roster_received = asyncio.Event() def roster_received_cb(self, event): self.roster_received.set() self.presences_received.clear() async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() self.get_roster(callback=self.roster_received_cb) print('Waiting for presence updates...\n') await self.roster_received.wait() print('Roster received') await self.presences_received.wait() self.disconnect() async def on_vcard_avatar(self, pres): print("Received vCard avatar update from %s" % pres['from'].bare) try: result = await self['xep_0054'].get_vcard(pres['from'].bare, cached=True, timeout=5) except XMPPError: print("Error retrieving avatar for %s" % pres['from']) return avatar = result['vcard_temp']['PHOTO'] filetype = FILE_TYPES.get(avatar['TYPE'], 'png') filename = 'vcard_avatar_%s_%s.%s' % ( pres['from'].bare, pres['vcard_temp_update']['photo'], filetype) with open(filename, 'wb+') as img: img.write(avatar['BINVAL']) async def on_avatar(self, msg): print("Received avatar update from %s" % msg['from']) metadata = msg['pubsub_event']['items']['item']['avatar_metadata'] for info in metadata['items']: if not info['url']: try: result = await self['xep_0084'].retrieve_avatar(msg['from'].bare, info['id'], timeout=5) except XMPPError: print("Error retrieving avatar for %s" % msg['from']) return avatar = result['pubsub']['items']['item']['avatar_data'] filetype = FILE_TYPES.get(metadata['type'], 'png') filename = 'avatar_%s_%s.%s' % (msg['from'].bare, info['id'], filetype) with open(filename, 'wb+') as img: img.write(avatar['value']) else: # We could retrieve the avatar via HTTP, etc here instead. pass def wait_for_presences(self, pres): """ Wait to receive updates from all roster contacts. """ self.received.add(pres['from'].bare) print((len(self.received), len(self.client_roster.keys()))) if len(self.received) >= len(self.client_roster.keys()): self.presences_received.set() else: self.presences_received.clear() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() parser.add_argument("-q","--quiet", help="set logging to ERROR", action="store_const", dest="loglevel", const=logging.ERROR, default=logging.ERROR) parser.add_argument("-d","--debug", help="set logging to DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.ERROR) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = AvatarDownloader(args.jid, args.password) xmpp.register_plugin('xep_0054') xmpp.register_plugin('xep_0153') xmpp.register_plugin('xep_0084') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/echo_client.py000077500000000000000000000073021477105560000172300ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class EchoBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will echo messages it receives, along with a short thank you message. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=EchoBot.__doc__) # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the EchoBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = EchoBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/echo_component.py000077500000000000000000000071541477105560000177610ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.componentxmpp import ComponentXMPP class EchoComponent(ComponentXMPP): """ A simple Slixmpp component that echoes messages. """ def __init__(self, jid, secret, server, port): ComponentXMPP.__init__(self, jid, secret, server, port) # You don't need a session_start handler, but that is # where you would broadcast initial presence. # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Since a component may send messages from any number of JIDs, it is best to always include a from JID. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ # The reply method will use the messages 'to' JID as the # outgoing reply's 'from' JID. msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=EchoComponent.__doc__) # Output verbosity options. 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) # JID and password options. 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("-s", "--server", dest="server", help="server to connect to") parser.add_argument("-P", "--port", dest="port", help="port to connect to") args = parser.parse_args() if args.jid is None: args.jid = input("Component JID: ") if args.password is None: args.password = getpass("Password: ") if args.server is None: args.server = input("Server: ") if args.port is None: args.port = int(input("Port: ")) # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') # Setup the EchoComponent and register plugins. Note that while plugins # may have interdependencies, the order in which you register them does # not matter. xmpp = EchoComponent(args.jid, args.password, args.server, args.port) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/gtalk_custom_domain.py000077500000000000000000000106521477105560000210010ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp import ssl from slixmpp.xmlstream import cert class GTalkBot(slixmpp.ClientXMPP): """ A demonstration of using Slixmpp with accounts from a Google Apps account with a custom domain, because it requires custom certificate validation. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) # Using a Google Apps custom domain, the certificate # does not contain the custom domain, just the GTalk # server name. So we will need to process invalid # certifcates ourselves and check that it really # is from Google. self.add_event_handler("ssl_invalid_cert", self.invalid_cert) def invalid_cert(self, pem_cert): der_cert = ssl.PEM_cert_to_DER_cert(pem_cert) try: cert.verify('talk.google.com', der_cert) logging.debug("CERT: Found GTalk certificate") except cert.CertificateError as err: logging.error(err.message) self.disconnect() async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the GTalkBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = GTalkBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/http_over_xmpp.py000066400000000000000000000055121477105560000200300ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Implementation of HTTP over XMPP transport # http://xmpp.org/extensions/xep-0332.html # Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com # This file is part of slixmpp. # See the file LICENSE for copying permission. import asyncio from slixmpp import ClientXMPP from argparse import ArgumentParser import logging import getpass class HTTPOverXMPPClient(ClientXMPP): def __init__(self, jid, password): ClientXMPP.__init__(self, jid, password) self.register_plugin('xep_0332') # HTTP over XMPP Transport self.add_event_handler( 'session_start', self.session_start ) self.add_event_handler('http_request', self.http_request_received) self.add_event_handler('http_response', self.http_response_received) def http_request_received(self, iq): pass def http_response_received(self, iq): print('HTTP Response Received : %s' % iq) print('From : %s' % iq['from']) print('To : %s' % iq['to']) print('Type : %s' % iq['type']) print('Headers : %s' % iq['resp']['headers']) print('Code : %s' % iq['resp']['code']) print('Message : %s' % iq['resp']['message']) print('Data : %s' % iq['resp']['data']) def session_start(self, event): # TODO: Fill in the blanks self['xep_0332'].send_request( to='?', method='?', resource='?', headers={} ) self.disconnect() if __name__ == '__main__': # # NOTE: To run this example, fill up the blanks in session_start() and # use the following command. # # ./http_over_xmpp.py -J -P -i -p [-v] # parser = ArgumentParser() # Output verbosity options. parser.add_argument( '-v', '--verbose', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.ERROR ) # JID and password options. parser.add_argument('-J', '--jid', dest='jid', help='JID') parser.add_argument('-P', '--password', dest='password', help='Password') # XMPP server ip and port options. parser.add_argument( '-i', '--ipaddr', dest='ipaddr', help='IP Address of the XMPP server', default=None ) parser.add_argument( '-p', '--port', dest='port', help='Port of the XMPP server', default=None ) args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input('Username: ') if args.password is None: args.password = getpass.getpass('Password: ') xmpp = HTTPOverXMPPClient(args.jid, args.password) xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/http_upload.py000077500000000000000000000116151477105560000173010ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2018 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import Optional import sys import logging from pathlib import Path from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp import JID from slixmpp.exceptions import IqTimeout log = logging.getLogger(__name__) class HttpUpload(slixmpp.ClientXMPP): """ A basic client asking an entity if they confirm the access to an HTTP URL. """ def __init__( self, jid: JID, password: str, recipient: JID, filename: Path, domain: Optional[JID] = None, encrypted: bool = False, ): slixmpp.ClientXMPP.__init__(self, jid, password) self.recipient = recipient self.filename = filename self.domain = domain self.encrypted = encrypted self.add_event_handler("session_start", self.start) async def start(self, event): log.info('Uploading file %s...', self.filename) try: upload_file = self['xep_0363'].upload_file if self.encrypted and not self['xep_0454']: print( 'The xep_0454 module isn\'t available. ' 'Ensure you have \'cryptography\' ' 'from extras_require installed.', file=sys.stderr, ) return elif self.encrypted: upload_file = self['xep_0454'].upload_file url = await upload_file( self.filename, domain=self.domain, timeout=10, ) except IqTimeout: raise TimeoutError('Could not send message in time') log.info('Upload success!') log.info('Sending file to %s', self.recipient) html = ( f'' f'{url}' ) message = self.make_message(mto=self.recipient, mbody=url, mhtml=html) message['oob']['url'] = url message.send() self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") # Other options. parser.add_argument("-r", "--recipient", required=True, help="Recipient JID") parser.add_argument("-f", "--file", required=True, help="File to send") parser.add_argument("--domain", help="Domain to use for HTTP File Upload (leave out for your own server’s)") parser.add_argument("-e", "--encrypt", dest="encrypted", help="Whether to encrypt", action="store_true", default=False) args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = JID(input("Username: ")) if args.password is None: args.password = getpass("Password: ") domain = args.domain if domain is not None: domain = JID(domain) if args.encrypted: print( 'You are using the --encrypt flag. ' 'Be aware that the transport being used is NOT end-to-end ' 'encrypted. The server will be able to decrypt the file.', file=sys.stderr, ) xmpp = HttpUpload( jid=args.jid, password=args.password, recipient=JID(args.recipient), filename=Path(args.file), domain=domain, encrypted=args.encrypted, ) xmpp.register_plugin('xep_0066') xmpp.register_plugin('xep_0071') xmpp.register_plugin('xep_0128') xmpp.register_plugin('xep_0363') try: xmpp.register_plugin('xep_0454') except slixmpp.plugins.base.PluginNotFound: log.error( 'Could not load xep_0454. ' 'Ensure you have \'cryptography\' from extras_require installed.' ) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/ibb_transfer/000077500000000000000000000000001477105560000170355ustar00rootroot00000000000000slixmpp/examples/ibb_transfer/ibb_receiver.py000077500000000000000000000071501477105560000220350ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class IBBReceiver(slixmpp.ClientXMPP): """ A basic example of creating and using an in-band bytestream. """ def __init__(self, jid, password, filename): slixmpp.ClientXMPP.__init__(self, jid, password) self.file = open(filename, 'wb') # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) self.add_event_handler("ibb_stream_start", self.stream_opened) self.add_event_handler("ibb_stream_data", self.stream_data) self.add_event_handler("ibb_stream_end", self.stream_closed) def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() self.get_roster() def stream_opened(self, stream): print('Stream opened: %s from %s' % (stream.sid, stream.peer_jid)) def stream_data(self, stream): self.file.write(stream.read()) def stream_closed(self, stream): print('Stream closed: %s from %s' % (stream.sid, stream.peer_jid)) self.file.close() self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-o", "--out", dest="filename", help="file to save to") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.filename is None: args.filename = input("File path: ") # Setup the IBBReceiver and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = IBBReceiver(args.jid, args.password, args.filename) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0047', { 'auto_accept': True }) # In-band Bytestreams # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/ibb_transfer/ibb_sender.py000077500000000000000000000102021477105560000215010ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import IqError, IqTimeout class IBBSender(slixmpp.ClientXMPP): """ A basic example of creating and using an in-band bytestream. """ def __init__(self, jid, password, receiver, filename, use_messages=False): slixmpp.ClientXMPP.__init__(self, jid, password) self.receiver = receiver self.file = open(filename, 'rb') self.use_messages = use_messages # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() self.get_roster() try: # Open the IBB stream in which to write to. stream = await self['xep_0047'].open_stream(self.receiver, use_messages=self.use_messages) # If you want to send in-memory bytes, use stream.sendall() instead. await stream.sendfile(self.file, timeout=10) # And finally close the stream. await stream.close(timeout=10) except (IqError, IqTimeout): print('File transfer errored') else: print('File transfer finished') finally: self.file.close() self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-r", "--receiver", dest="receiver", help="JID of the receiver") parser.add_argument("-f", "--file", dest="filename", help="file to send") parser.add_argument("-m", "--use-messages", action="store_true", help="use messages instead of iqs for file transfer") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.receiver is None: args.receiver = input("Receiver: ") if args.filename is None: args.filename = input("File path: ") # Setup the IBBSender and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = IBBSender(args.jid, args.password, args.receiver, args.filename, args.use_messages) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0047') # In-band Bytestreams # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/imghdr.py000066400000000000000000000077261477105560000162350ustar00rootroot00000000000000""" Recognize image file formats based on their first few bytes. Taken from cpython 3.11 source code before the removal in 3.13. Licensed under Zero-Clause BSD """ from os import PathLike import warnings __all__ = ["what"] warnings._deprecated(__name__, remove=(3, 13)) #-------------------------# # Recognize image headers # #-------------------------# def what(file, h=None): f = None try: if h is None: if isinstance(file, (str, PathLike)): f = open(file, 'rb') h = f.read(32) else: location = file.tell() h = file.read(32) file.seek(location) for tf in tests: res = tf(h, f) if res: return res finally: if f: f.close() return None #---------------------------------# # Subroutines per image file type # #---------------------------------# tests = [] def test_jpeg(h, f): """JPEG data with JFIF or Exif markers; and raw JPEG""" if h[6:10] in (b'JFIF', b'Exif'): return 'jpeg' elif h[:4] == b'\xff\xd8\xff\xdb': return 'jpeg' tests.append(test_jpeg) def test_png(h, f): if h.startswith(b'\211PNG\r\n\032\n'): return 'png' tests.append(test_png) def test_gif(h, f): """GIF ('87 and '89 variants)""" if h[:6] in (b'GIF87a', b'GIF89a'): return 'gif' tests.append(test_gif) def test_tiff(h, f): """TIFF (can be in Motorola or Intel byte order)""" if h[:2] in (b'MM', b'II'): return 'tiff' tests.append(test_tiff) def test_rgb(h, f): """SGI image library""" if h.startswith(b'\001\332'): return 'rgb' tests.append(test_rgb) def test_pbm(h, f): """PBM (portable bitmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r': return 'pbm' tests.append(test_pbm) def test_pgm(h, f): """PGM (portable graymap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r': return 'pgm' tests.append(test_pgm) def test_ppm(h, f): """PPM (portable pixmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r': return 'ppm' tests.append(test_ppm) def test_rast(h, f): """Sun raster file""" if h.startswith(b'\x59\xA6\x6A\x95'): return 'rast' tests.append(test_rast) def test_xbm(h, f): """X bitmap (X10 or X11)""" if h.startswith(b'#define '): return 'xbm' tests.append(test_xbm) def test_bmp(h, f): if h.startswith(b'BM'): return 'bmp' tests.append(test_bmp) def test_webp(h, f): if h.startswith(b'RIFF') and h[8:12] == b'WEBP': return 'webp' tests.append(test_webp) def test_exr(h, f): if h.startswith(b'\x76\x2f\x31\x01'): return 'exr' tests.append(test_exr) #--------------------# # Small test program # #--------------------# def test(): import sys recursive = 0 if sys.argv[1:] and sys.argv[1] == '-r': del sys.argv[1:2] recursive = 1 try: if sys.argv[1:]: testall(sys.argv[1:], recursive, 1) else: testall(['.'], recursive, 1) except KeyboardInterrupt: sys.stderr.write('\n[Interrupted]\n') sys.exit(1) def testall(list, recursive, toplevel): import sys import os for filename in list: if os.path.isdir(filename): print(filename + '/:', end=' ') if recursive or toplevel: print('recursing down:') import glob names = glob.glob(os.path.join(glob.escape(filename), '*')) testall(names, recursive, 0) else: print('*** directory (use -r) ***') else: print(filename + ':', end=' ') sys.stdout.flush() try: print(what(filename)) except OSError: print('*** not found ***') if __name__ == '__main__': test() slixmpp/examples/mam.py000077500000000000000000000061361477105560000155320ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2017 Mathieu Pasquet # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError log = logging.getLogger(__name__) class MAM(slixmpp.ClientXMPP): """ A basic client fetching mam archive messages """ def __init__(self, jid, password, remote_jid, start): slixmpp.ClientXMPP.__init__(self, jid, password) self.remote_jid = remote_jid self.start_date = start self.add_event_handler("session_start", self.start) async def start(self, *args): """ Fetch mam results for the specified JID. Use RSM to paginate the results. """ results = self.plugin['xep_0313'].retrieve(jid=self.remote_jid, iterator=True, rsm={'max': 10}, start=self.start_date) page = 1 async for rsm in results: print('Page %d' % page) for msg in rsm['mam']['results']: forwarded = msg['mam_result']['forwarded'] timestamp = forwarded['delay']['stamp'] message = forwarded['stanza'] print('[%s] %s: %s' % (timestamp, message['from'], message['body'])) page += 1 self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") # Other options parser.add_argument("-r", "--remote-jid", dest="remote_jid", help="Remote JID") parser.add_argument("--start", help="Start date", default='2017-09-20T12:00:00Z') args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.remote_jid is None: args.remote_jid = input("Remote JID: ") if args.start is None: args.start = input("Start time: ") xmpp = MAM(args.jid, args.password, args.remote_jid, args.start) xmpp.register_plugin('xep_0313') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/markup.py000077500000000000000000000077451477105560000162660ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.plugins.xep_0394 import stanza as markup_stanza class EchoBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will echo messages it receives, along with a short thank you message. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ body = msg['body'] new_body = self['xep_0394'].to_plain_text(body, msg['markup']) xhtml = self['xep_0394'].to_xhtml_im(body, msg['markup']) print('Plain text:', new_body) print('XHTML-IM:', xhtml['body']) message = msg.reply() message['body'] = new_body message['html']['body'] = xhtml['body'] self.send(message) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=EchoBot.__doc__) # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the EchoBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = EchoBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0394') # Message Markup # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/migrate_roster.py000077500000000000000000000065471477105560000200140ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("--oldjid", dest="old_jid", help="JID of the old account") parser.add_argument("--oldpassword", dest="old_password", help="password of the old account") parser.add_argument("--newjid", dest="new_jid", help="JID of the old account") parser.add_argument("--newpassword", dest="new_password", help="password of the old account") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.old_jid is None: args.old_jid = input("Old JID: ") if args.old_password is None: args.old_password = getpass("Old Password: ") if args.new_jid is None: args.new_jid = input("New JID: ") if args.new_password is None: args.new_password = getpass("New Password: ") old_xmpp = slixmpp.ClientXMPP(args.old_jid, args.old_password) # If you are connecting to Facebook and wish to use the # X-FACEBOOK-PLATFORM authentication mechanism, you will need # your API key and an access token. Then you'll set: # xmpp.credentials['api_key'] = 'THE_API_KEY' # xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN' # If you are connecting to MSN, then you will need an # access token, and it does not matter what JID you # specify other than that the domain is 'messenger.live.com', # so '_@messenger.live.com' will work. You can specify # the access token as so: # xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN' # If you are working with an OpenFire server, you may need # to adjust the SSL version used: # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 # If you want to verify the SSL certificates offered by a server: # xmpp.ca_certs = "path/to/ca/cert" roster = [] async def on_session(event): roster.append(await old_xmpp.get_roster()) old_xmpp.disconnect() old_xmpp.add_event_handler('session_start', on_session) if old_xmpp.connect(): asyncio.get_event_loop().run_until_complete(old_xmpp.disconnected) if not roster: print('No roster to migrate') sys.exit() new_xmpp = slixmpp.ClientXMPP(args.new_jid, args.new_password) async def on_session2(event): await new_xmpp.get_roster() new_xmpp.send_presence() logging.info(roster[0]) data = roster[0]['roster']['items'] logging.info(data) for jid, item in data.items(): if item['subscription'] != 'none': new_xmpp.send_presence(ptype='subscribe', pto=jid) await new_xmpp.update_roster( jid, name=item['name'], groups=item['groups'] ) new_xmpp.disconnect() new_xmpp.add_event_handler('session_start', on_session2) new_xmpp.connect() asyncio.get_event_loop().run_until_complete(new_xmpp.disconnected) slixmpp/examples/mix.py000077500000000000000000000134001477105560000155450ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2021 Mathieu Pasquet # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class MIXBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will greets those who enter the room, and acknowledge any messages that mentions the bot's nickname. """ def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.rooms = set() self.nick = nick # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The mix_message event is triggered whenever a message # stanza is received from any chat room. self.add_event_handler("mix_message", self.mix_message) # The mix_participant_info_publish event is triggered whenever # an occupant joins or leaves the channel (not linked to # actual presence) self.add_event_handler("mix_participant_info_publish", self.mix_joined) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. """ # The goal here is to fetch the already joined MIX channels # which are present in the roster. _, mix_rooms = await self.plugin['xep_0405'].get_mix_roster() for room in mix_rooms: self.rooms.add(room['jid']) self.send_presence() if self.room not in self.rooms: # If we are not joined, we need to. This will carry over # the next restarts await self.plugin['xep_0405'].join_channel( self.room, self.nick, ) def mix_message(self, msg): """ Process incoming message stanzas from any chat room. Be aware that if you also have any handlers for the 'message' event, message stanzas may be processed by both handlers, so check the 'type' attribute when using a 'message' event handler. Whenever the bot's nickname is mentioned, respond to the message. IMPORTANT: Always check that a message is not from yourself, otherwise you will create an infinite loop responding to your own messages. This handler will reply to messages that mention the bot's nickname. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['mix']['nick'] != self.nick and self.nick in msg['body']: self.send_message(mto=msg['from'].bare, mbody="I heard that, %s." % msg['mix']['nick'], mtype='groupchat') def mix_joined(self, event): """ We receive a publish event whenever someone joins the MIX channel. It contains the nickname of the new participant, and the JID when the channel is not a "JID Hidden channel". """ participant = event['pubsub_event']['items']['item']['mix_participant'] if participant['nick'] != self.nick: self.send_message(mto=event['from'].bare, mbody="Hello, %s" % participant['nick'], mtype='groupchat') if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-r", "--room", dest="room", help="MIX channel to join") parser.add_argument("-n", "--nick", dest="nick", help="MIX nickname") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.room is None: args.room = input("MIX channel: ") if args.nick is None: args.nick = input("MIX nickname: ") # Setup the MIXBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = MIXBot(args.jid, args.password, args.room, args.nick) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0369') # MIX Core xmpp.register_plugin('xep_0405') # MIX PAM # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/muc.py000077500000000000000000000143361477105560000155450ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class MUCBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will greets those who enter the room, and acknowledge any messages that mentions the bot's nickname. """ def __init__(self, jid, password, room, nick): slixmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The groupchat_message event is triggered whenever a message # stanza is received from any chat room. If you also also # register a handler for the 'message' event, MUC messages # will be processed by both handlers. self.add_event_handler("groupchat_message", self.muc_message) # The groupchat_presence event is triggered whenever a # presence stanza is received from any chat room, including # any presences you send yourself. To limit event handling # to a single room, use the events muc::room@server::presence, # muc::room@server::got_online, or muc::room@server::got_offline. self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ await self.get_roster() self.send_presence() self.plugin['xep_0045'].join_muc(self.room, self.nick, # If a room password is needed, use: # password=the_room_password, ) def muc_message(self, msg): """ Process incoming message stanzas from any chat room. Be aware that if you also have any handlers for the 'message' event, message stanzas may be processed by both handlers, so check the 'type' attribute when using a 'message' event handler. Whenever the bot's nickname is mentioned, respond to the message. IMPORTANT: Always check that a message is not from yourself, otherwise you will create an infinite loop responding to your own messages. This handler will reply to messages that mention the bot's nickname. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['mucnick'] != self.nick and self.nick in msg['body']: self.send_message(mto=msg['from'].bare, mbody="I heard that, %s." % msg['mucnick'], mtype='groupchat') def muc_online(self, presence): """ Process a presence stanza from a chat room. In this case, presences from users that have just come online are handled by sending a welcome message that includes the user's nickname and role in the room. Arguments: presence -- The received presence stanza. See the documentation for the Presence stanza to see how else it may be used. """ if presence['muc']['nick'] != self.nick: self.send_message(mto=presence['from'].bare, mbody="Hello, %s %s" % (presence['muc']['role'], presence['muc']['nick']), mtype='groupchat') if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-r", "--room", dest="room", help="MUC room to join") parser.add_argument("-n", "--nick", dest="nick", help="MUC nickname") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.room is None: args.room = input("MUC room: ") if args.nick is None: args.nick = input("MUC nickname: ") # Setup the MUCBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = MUCBot(args.jid, args.password, args.room, args.nick) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/ping.py000077500000000000000000000072121477105560000157110ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser from slixmpp.exceptions import IqError, IqTimeout import asyncio import slixmpp class PingTest(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will send a ping request to a given JID. """ def __init__(self, jid, password, pingjid): slixmpp.ClientXMPP.__init__(self, jid, password) if pingjid is None: pingjid = self.boundjid.bare self.pingjid = pingjid # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() try: rtt = await self['xep_0199'].ping(self.pingjid, timeout=10) logging.info("Success! RTT: %s", rtt) except IqError as e: logging.info("Error pinging %s: %s", self.pingjid, e.iq['error']['condition']) except IqTimeout: logging.info("No response from %s", self.pingjid) finally: self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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("-t", "--pingto", help="set jid to ping", dest="pingjid", default=None) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the PingTest and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = PingTest(args.jid, args.password, args.pingjid) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/pubsub_client.py000077500000000000000000000154161477105560000176170ustar00rootroot00000000000000#!/usr/bin/env python3 import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import ET, tostring class PubsubClient(slixmpp.ClientXMPP): def __init__(self, jid, password, server, node=None, action='nodes', data=''): super().__init__(jid, password) self.register_plugin('xep_0030') self.register_plugin('xep_0059') self.register_plugin('xep_0060') self.actions = ['nodes', 'create', 'delete', 'get_configure', 'publish', 'get', 'retract', 'purge', 'subscribe', 'unsubscribe'] self.action = action self.node = node self.data = data self.pubsub_server = server self.add_event_handler('session_start', self.start) async def start(self, event): await self.get_roster() self.send_presence() try: await getattr(self, self.action)() except: logging.exception('Could not execute %s:', self.action) self.disconnect() async def nodes(self): try: result = await self['xep_0060'].get_nodes(self.pubsub_server, self.node) for item in result['disco_items']['items']: logging.info(' - %s', str(item)) except XMPPError as error: logging.error('Could not retrieve node list: %s', error.format()) async def create(self): try: await self['xep_0060'].create_node(self.pubsub_server, self.node) logging.info('Created node %s', self.node) except XMPPError as error: logging.error('Could not create node %s: %s', self.node, error.format()) async def delete(self): try: await self['xep_0060'].delete_node(self.pubsub_server, self.node) logging.info('Deleted node %s', self.node) except XMPPError as error: logging.error('Could not delete node %s: %s', self.node, error.format()) async def get_configure(self): try: configuration_form = await self['xep_0060'].get_node_config(self.pubsub_server, self.node) logging.info('Configure form received from node %s: %s', self.node, configuration_form['pubsub_owner']['configure']['form']) except XMPPError as error: logging.error('Could not retrieve configure form from node %s: %s', self.node, error.format()) async def publish(self): payload = ET.fromstring("%s" % self.data) try: result = await self['xep_0060'].publish(self.pubsub_server, self.node, payload=payload) logging.info('Published at item id: %s', result['pubsub']['publish']['item']['id']) except XMPPError as error: logging.error('Could not publish to %s: %s', self.node, error.format()) async def get(self): try: result = await self['xep_0060'].get_item(self.pubsub_server, self.node, self.data) for item in result['pubsub']['items']['substanzas']: logging.info('Retrieved item %s: %s', item['id'], tostring(item['payload'])) except XMPPError as error: logging.error('Could not retrieve item %s from node %s: %s', self.data, self.node, error.format()) async def retract(self): try: await self['xep_0060'].retract(self.pubsub_server, self.node, self.data) logging.info('Retracted item %s from node %s', self.data, self.node) except XMPPError as error: logging.error('Could not retract item %s from node %s: %s', self.data, self.node, error.format()) async def purge(self): try: await self['xep_0060'].purge(self.pubsub_server, self.node) logging.info('Purged all items from node %s', self.node) except XMPPError as error: logging.error('Could not purge items from node %s: %s', self.node, error.format()) async def subscribe(self): try: iq = await self['xep_0060'].subscribe(self.pubsub_server, self.node) subscription = iq['pubsub']['subscription'] logging.info('Subscribed %s to node %s', subscription['jid'], subscription['node']) except XMPPError as error: logging.error('Could not subscribe %s to node %s: %s', self.boundjid.bare, self.node, error.format()) async def unsubscribe(self): try: await self['xep_0060'].unsubscribe(self.pubsub_server, self.node) logging.info('Unsubscribed %s from node %s', self.boundjid.bare, self.node) except XMPPError as error: logging.error('Could not unsubscribe %s from node %s: %s', self.boundjid.bare, self.node, error.format()) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() parser.version = '%%prog 0.1' parser.usage = "Usage: %%prog [options] " + \ 'nodes|create|delete|get_configure|purge|subscribe|unsubscribe|publish|retract|get' + \ ' [ ]' 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) # JID and password options. 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("server") parser.add_argument("action", choices=["nodes", "create", "delete", "get_configure", "purge", "subscribe", "unsubscribe", "publish", "retract", "get"]) parser.add_argument("node", nargs='?') parser.add_argument("data", nargs='?') args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the Pubsub client xmpp = PubsubClient(args.jid, args.password, server=args.server, node=args.node, action=args.action, data=args.data) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/pubsub_events.py000077500000000000000000000106471477105560000176460ustar00rootroot00000000000000#!/usr/bin/env python3 import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.xmlstream import ET, tostring from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.handler import Callback class PubsubEvents(slixmpp.ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) self.register_plugin('xep_0030') self.register_plugin('xep_0059') self.register_plugin('xep_0060') self.add_event_handler('session_start', self.start) # Some services may require configuration to allow # sending delete, configuration, or subscription events. self.add_event_handler('pubsub_publish', self._publish) self.add_event_handler('pubsub_retract', self._retract) self.add_event_handler('pubsub_purge', self._purge) self.add_event_handler('pubsub_delete', self._delete) self.add_event_handler('pubsub_config', self._config) self.add_event_handler('pubsub_subscription', self._subscription) # Want to use nicer, more specific pubsub event names? # self['xep_0060'].map_node_event('node_name', 'event_prefix') # self.add_event_handler('event_prefix_publish', handler) # self.add_event_handler('event_prefix_retract', handler) # self.add_event_handler('event_prefix_purge', handler) # self.add_event_handler('event_prefix_delete', handler) async def start(self, event): await self.get_roster() self.send_presence() def _publish(self, msg): """Handle receiving a publish item event.""" print('Published item %s to %s:' % ( msg['pubsub_event']['items']['item']['id'], msg['pubsub_event']['items']['node'])) data = msg['pubsub_event']['items']['item']['payload'] if data is not None: print(tostring(data)) else: print('No item content') def _retract(self, msg): """Handle receiving a retract item event.""" print('Retracted item %s from %s' % ( msg['pubsub_event']['items']['retract']['id'], msg['pubsub_event']['items']['node'])) def _purge(self, msg): """Handle receiving a node purge event.""" print('Purged all items from %s' % ( msg['pubsub_event']['purge']['node'])) def _delete(self, msg): """Handle receiving a node deletion event.""" print('Deleted node %s' % ( msg['pubsub_event']['delete']['node'])) def _config(self, msg): """Handle receiving a node configuration event.""" print('Configured node %s:' % ( msg['pubsub_event']['configuration']['node'])) print(msg['pubsub_event']['configuration']['form']) def _subscription(self, msg): """Handle receiving a node subscription event.""" print('Subscription change for node %s:' % ( msg['pubsub_event']['subscription']['node'])) print(msg['pubsub_event']['subscription']) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") logging.info("Run this in conjunction with the pubsub_client.py " + \ "example to watch events happen as you give commands.") # Setup the PubsubEvents listener xmpp = PubsubEvents(args.jid, args.password) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/register_account.py000077500000000000000000000120701477105560000203120ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import IqError, IqTimeout class RegisterBot(slixmpp.ClientXMPP): """ A basic bot that will attempt to register an account with an XMPP server. NOTE: This follows the very basic registration workflow from XEP-0077. More advanced server registration workflows will need to check for data forms, etc. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The register event provides an Iq result stanza with # a registration form from the server. This may include # the basic registration fields, a data form, an # out-of-band URL, or any combination. For more advanced # cases, you will need to examine the fields provided # and respond accordingly. Slixmpp provides plugins # for data forms and OOB links that will make that easier. self.add_event_handler("register", self.register) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() # We're only concerned about registering, so nothing more to do here. self.disconnect() async def register(self, iq): """ Fill out and submit a registration form. The form may be composed of basic registration fields, a data form, an out-of-band link, or any combination thereof. Data forms and OOB links can be checked for as so: if iq.match('iq/register/form'): # do stuff with data form # iq['register']['form']['fields'] if iq.match('iq/register/oob'): # do stuff with OOB URL # iq['register']['oob']['url'] To get the list of basic registration fields, you can use: iq['register']['fields'] """ resp = self.Iq() resp['type'] = 'set' resp['register']['username'] = self.boundjid.user resp['register']['password'] = self.password try: await resp.send() logging.info("Account created for %s!" % self.boundjid) except IqError as e: logging.error("Could not register account: %s" % e.iq['error']['text']) self.disconnect() except IqTimeout: logging.error("No response from server.") self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") # Setup the RegisterBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = RegisterBot(args.jid, args.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data forms xmpp.register_plugin('xep_0066') # Out-of-band Data xmpp.register_plugin('xep_0077') # In-band Registration # Some servers don't advertise support for inband registration, even # though they allow it. If this applies to your server, use: xmpp['xep_0077'].force_registration = True # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/roster_browser.py000077500000000000000000000105561477105560000200420ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import IqError, IqTimeout from slixmpp.xmlstream.asyncio import asyncio class RosterBrowser(slixmpp.ClientXMPP): """ A basic script for dumping a client's roster to the command line. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) self.add_event_handler("changed_status", self.wait_for_presences) self.received = set() self.presences_received = asyncio.Event() async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ try: await self.get_roster() except IqError as err: print('Error: %s' % err.iq['error']['condition']) except IqTimeout: print('Error: Request timed out') self.send_presence() print('Waiting for presence updates...\n') await asyncio.sleep(10) print('Roster for %s' % self.boundjid.bare) groups = self.client_roster.groups() for group in groups: print('\n%s' % group) print('-' * 72) for jid in groups[group]: sub = self.client_roster[jid]['subscription'] name = self.client_roster[jid]['name'] if self.client_roster[jid]['name']: print(' %s (%s) [%s]' % (name, jid, sub)) else: print(' %s [%s]' % (jid, sub)) connections = self.client_roster.presence(jid) for res, pres in connections.items(): show = 'available' if pres['show']: show = pres['show'] print(' - %s (%s)' % (res, show)) if pres['status']: print(' %s' % pres['status']) self.disconnect() def wait_for_presences(self, pres): """ Track how many roster entries have received presence updates. """ self.received.add(pres['from'].bare) if len(self.received) >= len(self.client_roster.keys()): self.presences_received.set() else: self.presences_received.clear() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() parser.add_argument("-q","--quiet", help="set logging to ERROR", action="store_const", dest="loglevel", const=logging.ERROR, default=logging.ERROR) parser.add_argument("-d","--debug", help="set logging to DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.ERROR) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = RosterBrowser(args.jid, args.password) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/rpc_client_side.py000077500000000000000000000046331477105560000201060ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Dann Martens # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from argparse import ArgumentParser from getpass import getpass from slixmpp.jid import JID from slixmpp.plugins.xep_0009.remote import Endpoint, remote, Remote class Thermostat(Endpoint): def FQN(self): return 'thermostat' def __init__(self, initial_temperature): pass @remote def set_temperature(self, temperature): return NotImplemented @remote def get_temperature(self): return NotImplemented @remote(False) def release(self): return NotImplemented async def main(jid: JID, password: str, target_jid: JID): session = await Remote.new_session(jid, password) thermostat = session.new_proxy(target_jid, Thermostat) print("Current temperature is %s" % await thermostat.get_temperature()) await thermostat.set_temperature(20) print("Current temperature is %s" % await thermostat.get_temperature()) session.close() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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", "--target-jid", dest="target_jid", help="target JID to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.target_jid is None: args.target_jid = input("Target jid: ") asyncio.run(main(JID(args.jid), args.password, args.target_jid)) slixmpp/examples/rpc_server_side.py000077500000000000000000000043751477105560000201410ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Dann Martens # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from argparse import ArgumentParser from getpass import getpass from slixmpp.jid import JID from slixmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ ANY_ALL class Thermostat(Endpoint): def FQN(self): return 'thermostat' def __init__(self, initial_temperature): self._temperature = initial_temperature self._event = asyncio.Event() @remote def set_temperature(self, temperature): print("Setting temperature to %s" % temperature) self._temperature = temperature @remote def get_temperature(self): return self._temperature @remote(False) def release(self): self._event.set() async def wait_for_release(self): await self._event.wait() async def main(jid: JID, password: str): session = await Remote.new_session(jid, password) thermostat = session.new_handler(ANY_ALL, Thermostat, 18) await thermostat.wait_for_release() session.close() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") asyncio.run(main(JID(args.jid), args.password)) slixmpp/examples/s5b_transfer/000077500000000000000000000000001477105560000167725ustar00rootroot00000000000000slixmpp/examples/s5b_transfer/s5b_receiver.py000077500000000000000000000054331477105560000217310ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class S5BReceiver(slixmpp.ClientXMPP): """ A basic example of creating and using a SOCKS5 bytestream. """ def __init__(self, jid, password, filename): slixmpp.ClientXMPP.__init__(self, jid, password) self.file = open(filename, 'wb') self.add_event_handler("socks5_connected", self.stream_opened) self.add_event_handler("socks5_data", self.stream_data) self.add_event_handler("socks5_closed", self.stream_closed) def stream_opened(self, sid): logging.info('Stream opened. %s', sid) def stream_data(self, data): self.file.write(data) def stream_closed(self, exception): logging.info('Stream closed. %s', exception) self.file.close() self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-o", "--out", dest="filename", help="file to save to") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.filename is None: args.filename = input("File path: ") # Setup the S5BReceiver and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = S5BReceiver(args.jid, args.password, args.filename) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0065', { 'auto_accept': True }) # SOCKS5 Bytestreams # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/s5b_transfer/s5b_sender.py000077500000000000000000000076651477105560000214160ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import IqError, IqTimeout class S5BSender(slixmpp.ClientXMPP): """ A basic example of creating and using a SOCKS5 bytestream. """ def __init__(self, jid, password, receiver, filename): slixmpp.ClientXMPP.__init__(self, jid, password) self.receiver = receiver self.file = open(filename, 'rb') # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ try: # Open the S5B stream in which to write to. proxy = await self['xep_0065'].handshake(self.receiver) # Send the entire file. while True: data = self.file.read(1048576) if not data: break await proxy.write(data) # And finally close the stream. proxy.transport.write_eof() except (IqError, IqTimeout): print('File transfer errored') else: print('File transfer finished') finally: self.file.close() self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. 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("-r", "--receiver", dest="receiver", help="JID of the receiver") parser.add_argument("-f", "--file", dest="filename", help="file to send") parser.add_argument("-m", "--use-messages", action="store_true", help="use messages instead of iqs for file transfer") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.receiver is None: args.receiver = input("Receiver: ") if args.filename is None: args.filename = input("File path: ") # Setup the S5BSender and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = S5BSender(args.jid, args.password, args.receiver, args.filename) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0065') # SOCKS5 Bytestreams # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/send_client.py000077500000000000000000000067711477105560000172540ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp class SendMsgBot(slixmpp.ClientXMPP): """ A basic Slixmpp bot that will log in, send a message, and then log out. """ def __init__(self, jid, password, recipient, message): slixmpp.ClientXMPP.__init__(self, jid, password) # The message we wish to send, and the JID that # will receive it. self.recipient = recipient self.msg = message # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() self.send_message(mto=self.recipient, mbody=self.msg, mtype='chat') self.disconnect() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser(description=SendMsgBot.__doc__) # Output verbosity options. 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) # JID and password options. 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") parser.add_argument("-m", "--message", dest="message", help="message to send") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.to is None: args.to = input("Send To: ") if args.message is None: args.message = input("Message: ") # Setup the EchoBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = SendMsgBot(args.jid, args.password, args.to, args.message) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/set_avatar.py000077500000000000000000000105671477105560000171140ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import os import imghdr import logging from getpass import getpass from argparse import ArgumentParser import asyncio import slixmpp from slixmpp.exceptions import XMPPError class AvatarSetter(slixmpp.ClientXMPP): """ A basic script for downloading the avatars for a user's contacts. """ def __init__(self, jid, password, filepath): slixmpp.ClientXMPP.__init__(self, jid, password) self.add_event_handler("session_start", self.start) self.filepath = filepath async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() avatar_file = None try: avatar_file = open(os.path.expanduser(self.filepath), 'rb') except IOError: print('Could not find file: %s' % self.filepath) return self.disconnect() avatar = avatar_file.read() avatar_type = 'image/%s' % imghdr.what('', avatar) avatar_id = self['xep_0084'].generate_id(avatar) avatar_bytes = len(avatar) avatar_file.close() used_xep84 = False print('Publish XEP-0084 avatar data') result = await self['xep_0084'].publish_avatar(avatar) if isinstance(result, XMPPError): print('Could not publish XEP-0084 avatar') else: used_xep84 = True print('Update vCard with avatar') result = await self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type) if isinstance(result, XMPPError): print('Could not set vCard avatar') if used_xep84: print('Advertise XEP-0084 avatar metadata') result = await self['xep_0084'].publish_avatar_metadata([ {'id': avatar_id, 'type': avatar_type, 'bytes': avatar_bytes} # We could advertise multiple avatars to provide # options in image type, source (HTTP vs pubsub), # size, etc. # {'id': ....} ]) if isinstance(result, XMPPError): print('Could not publish XEP-0084 metadata') print('Wait for presence updates to propagate...') self.schedule('end', 5, self.disconnect, kwargs={'wait': True}) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() parser.add_argument("-q","--quiet", help="set logging to ERROR", action="store_const", dest="loglevel", const=logging.ERROR, default=logging.ERROR) parser.add_argument("-d","--debug", help="set logging to DEBUG", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.ERROR) # JID and password options. 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("-f", "--file", dest="filepath", help="path to the avatar file") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") if args.filepath is None: args.filepath = input("Avatar file location: ") xmpp = AvatarSetter(args.jid, args.password, args.filepath) xmpp.register_plugin('xep_0054') xmpp.register_plugin('xep_0153') xmpp.register_plugin('xep_0084') # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_until_complete(xmpp.disconnected) slixmpp/examples/thirdparty_auth.py000077500000000000000000000161201477105560000201650ustar00rootroot00000000000000#!/usr/bin/env python3 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import sys import logging from getpass import getpass from argparse import ArgumentParser try: from httplib import HTTPSConnection from urllib import urlencode except ImportError: from urllib.parse import urlencode from http.client import HTTPSConnection import asyncio import slixmpp from slixmpp.xmlstream import JID class ThirdPartyAuthBot(slixmpp.ClientXMPP): """ A simple Slixmpp bot that will echo messages it receives, along with a short thank you message. This version uses a thirdpary service for authentication, such as Facebook or Google. """ def __init__(self, jid, password): slixmpp.ClientXMPP.__init__(self, jid, password) # The X-GOOGLE-TOKEN mech is ranked lower than PLAIN # due to Google only allowing a single SASL attempt per # connection. So PLAIN will be used for TLS connections, # and X-GOOGLE-TOKEN for non-TLS connections. To use # X-GOOGLE-TOKEN with a TLS connection, explicitly select # it using: # # slixmpp.ClientXMPP.__init__(self, jid, password, # sasl_mech="X-GOOGLE-TOKEN") # The session_start event will be triggered when # the bot establishes its connection with the server # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. self.add_event_handler("session_start", self.start) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) async def start(self, event): """ Process the session_start event. Typical actions for the session_start event are requesting the roster and broadcasting an initial presence stanza. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.send_presence() await self.get_roster() def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['type'] in ('chat', 'normal'): msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") access_token = None # Since documentation on how to work with Google tokens # can be difficult to find, we'll demo a basic version # here. Note that responses could refer to a Captcha # URL that would require a browser. # Using Facebook or MSN's custom authentication requires # a browser, but the process is the same once a token # has been retrieved. # Request an access token from Google: try: conn = HTTPSConnection('www.google.com') except: print('Could not connect to Google') sys.exit() params = urlencode({ 'accountType': 'GOOGLE', 'service': 'mail', 'Email': JID(args.jid).bare, 'Passwd': args.password }) headers = { 'Content-Type': 'application/x-www-form-urlencoded' } try: conn.request('POST', '/accounts/ClientLogin', params, headers) resp = conn.getresponse().read() data = {} for line in resp.split(): k, v = line.split(b'=', 1) data[k] = v except Exception as e: print('Could not retrieve login data') sys.exit() if b'SID' not in data: print('Required data not found') sys.exit() params = urlencode({ 'SID': data[b'SID'], 'LSID': data[b'LSID'], 'service': 'mail' }) try: conn.request('POST', '/accounts/IssueAuthToken', params, headers) resp = conn.getresponse() data = resp.read().split() except: print('Could not retrieve auth data') sys.exit() if not data: print('Could not retrieve token') sys.exit() access_token = data[0] # Setup the ThirdPartyAuthBot and register plugins. Note that while plugins # may have interdependencies, the order in which you register them does not # matter. # If using MSN, the JID should be "user@messenger.live.com", which will # be overridden on session bind. # We're using an access token instead of a password, so we'll use `''` as # a password argument filler. xmpp = ThirdPartyAuthBot(args.jid, '') xmpp.credentials['access_token'] = access_token # The credentials dictionary is used to provide additional authentication # information beyond just a password. xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0060') # PubSub # MSN will kill connections that have been inactive for even # short periods of time. So use pings to keep the session alive; # whitespace keepalives do not work. xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency': 60}) # If you are working with an OpenFire server, you may need # to adjust the SSL version used: # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 # If you want to verify the SSL certificates offered by a server: # xmpp.ca_certs = "path/to/ca/cert" # Connect to the XMPP server and start processing XMPP stanzas. # Google only allows one SASL attempt per connection, so in order to # enable the X-GOOGLE-TOKEN mechanism, we'll disable TLS. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/user_location.py000077500000000000000000000062531477105560000176260ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import logging from getpass import getpass from argparse import ArgumentParser try: import json except ImportError: import simplejson as json try: import requests except ImportError: print('This demo requires the requests package for using HTTP.') sys.exit() import asyncio from slixmpp import ClientXMPP class LocationBot(ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) self.add_event_handler('session_start', self.start) self.add_event_handler('user_location_publish', self.user_location_publish) self.register_plugin('xep_0004') self.register_plugin('xep_0030') self.register_plugin('xep_0060') self.register_plugin('xep_0115') self.register_plugin('xep_0128') self.register_plugin('xep_0163') self.register_plugin('xep_0080') self.current_tune = None async def start(self, event): self.send_presence() await self.get_roster() self['xep_0115'].update_caps() print("Using freegeoip.net to get geolocation.") r = requests.get('http://freegeoip.net/json/') try: data = json.loads(r.text) except: print("Could not retrieve user location.") self.disconnect() return self['xep_0080'].publish_location( lat=data['latitude'], lon=data['longitude'], locality=data['city'], region=data['region_name'], country=data['country_name'], countrycode=data['country_code'], postalcode=data['zipcode']) def user_location_publish(self, msg): geo = msg['pubsub_event']['items']['item']['geoloc'] print("%s is at:" % msg['from']) for key, val in geo.values.items(): if val: print(" %s: %s" % (key, val)) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = LocationBot(args.jid, args.password) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/examples/user_tune.py000077500000000000000000000076221477105560000167720ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import logging from getpass import getpass from argparse import ArgumentParser try: from appscript import * except ImportError: print('This demo requires the appscript package to interact with iTunes.') sys.exit() import asyncio from slixmpp import ClientXMPP class TuneBot(ClientXMPP): def __init__(self, jid, password): super().__init__(jid, password) # Check for the current song every 5 seconds. self.schedule('Check Current Tune', 5, self._update_tune, repeat=True) self.add_event_handler('session_start', self.start) self.add_event_handler('user_tune_publish', self.user_tune_publish) self.register_plugin('xep_0004') self.register_plugin('xep_0030') self.register_plugin('xep_0060') self.register_plugin('xep_0115') self.register_plugin('xep_0118') self.register_plugin('xep_0128') self.register_plugin('xep_0163') self.current_tune = None async def start(self, event): self.send_presence() await self.get_roster() self['xep_0115'].update_caps() def _update_tune(self): itunes_count = app('System Events').processes[its.name == 'iTunes'].count() if itunes_count > 0: iTunes = app('iTunes') if iTunes.player_state.get() == k.playing: track = iTunes.current_track.get() length = track.time.get() if ':' in length: minutes, secs = map(int, length.split(':')) secs += minutes * 60 else: secs = int(length) artist = track.artist.get() title = track.name.get() source = track.album.get() rating = track.rating.get() / 10 tune = (artist, secs, rating, source, title) if tune != self.current_tune: self.current_tune = tune # We have a new song playing, so publish it. self['xep_0118'].publish_tune( artist=artist, length=secs, title=title, rating=rating, source=source) else: # No song is playing, clear the user tune. tune = None if tune != self.current_tune: self.current_tune = tune self['xep_0118'].stop() def user_tune_publish(self, msg): tune = msg['pubsub_event']['items']['item']['tune'] print("%s is listening to: %s" % (msg['from'], tune['title'])) if __name__ == '__main__': # Setup the command line arguments. parser = ArgumentParser() # Output verbosity options. 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) # JID and password options. parser.add_argument("-j", "--jid", dest="jid", help="JID to use") parser.add_argument("-p", "--password", dest="password", help="password to use") args = parser.parse_args() # Setup logging. logging.basicConfig(level=args.loglevel, format='%(levelname)-8s %(message)s') if args.jid is None: args.jid = input("Username: ") if args.password is None: args.password = getpass("Password: ") xmpp = TuneBot(args.jid, args.password) # Connect to the XMPP server and start processing XMPP stanzas. xmpp.connect() asyncio.get_event_loop().run_forever() slixmpp/itests/000077500000000000000000000000001477105560000140725ustar00rootroot00000000000000slixmpp/itests/__init__.py000066400000000000000000000000001477105560000161710ustar00rootroot00000000000000slixmpp/itests/imghdr.py000066400000000000000000000077261477105560000157320ustar00rootroot00000000000000""" Recognize image file formats based on their first few bytes. Taken from cpython 3.11 source code before the removal in 3.13. Licensed under Zero-Clause BSD """ from os import PathLike import warnings __all__ = ["what"] warnings._deprecated(__name__, remove=(3, 13)) #-------------------------# # Recognize image headers # #-------------------------# def what(file, h=None): f = None try: if h is None: if isinstance(file, (str, PathLike)): f = open(file, 'rb') h = f.read(32) else: location = file.tell() h = file.read(32) file.seek(location) for tf in tests: res = tf(h, f) if res: return res finally: if f: f.close() return None #---------------------------------# # Subroutines per image file type # #---------------------------------# tests = [] def test_jpeg(h, f): """JPEG data with JFIF or Exif markers; and raw JPEG""" if h[6:10] in (b'JFIF', b'Exif'): return 'jpeg' elif h[:4] == b'\xff\xd8\xff\xdb': return 'jpeg' tests.append(test_jpeg) def test_png(h, f): if h.startswith(b'\211PNG\r\n\032\n'): return 'png' tests.append(test_png) def test_gif(h, f): """GIF ('87 and '89 variants)""" if h[:6] in (b'GIF87a', b'GIF89a'): return 'gif' tests.append(test_gif) def test_tiff(h, f): """TIFF (can be in Motorola or Intel byte order)""" if h[:2] in (b'MM', b'II'): return 'tiff' tests.append(test_tiff) def test_rgb(h, f): """SGI image library""" if h.startswith(b'\001\332'): return 'rgb' tests.append(test_rgb) def test_pbm(h, f): """PBM (portable bitmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r': return 'pbm' tests.append(test_pbm) def test_pgm(h, f): """PGM (portable graymap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r': return 'pgm' tests.append(test_pgm) def test_ppm(h, f): """PPM (portable pixmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r': return 'ppm' tests.append(test_ppm) def test_rast(h, f): """Sun raster file""" if h.startswith(b'\x59\xA6\x6A\x95'): return 'rast' tests.append(test_rast) def test_xbm(h, f): """X bitmap (X10 or X11)""" if h.startswith(b'#define '): return 'xbm' tests.append(test_xbm) def test_bmp(h, f): if h.startswith(b'BM'): return 'bmp' tests.append(test_bmp) def test_webp(h, f): if h.startswith(b'RIFF') and h[8:12] == b'WEBP': return 'webp' tests.append(test_webp) def test_exr(h, f): if h.startswith(b'\x76\x2f\x31\x01'): return 'exr' tests.append(test_exr) #--------------------# # Small test program # #--------------------# def test(): import sys recursive = 0 if sys.argv[1:] and sys.argv[1] == '-r': del sys.argv[1:2] recursive = 1 try: if sys.argv[1:]: testall(sys.argv[1:], recursive, 1) else: testall(['.'], recursive, 1) except KeyboardInterrupt: sys.stderr.write('\n[Interrupted]\n') sys.exit(1) def testall(list, recursive, toplevel): import sys import os for filename in list: if os.path.isdir(filename): print(filename + '/:', end=' ') if recursive or toplevel: print('recursing down:') import glob names = glob.glob(os.path.join(glob.escape(filename), '*')) testall(names, recursive, 0) else: print('*** directory (use -r) ***') else: print(filename + ':', end=' ') sys.stdout.flush() try: print(what(filename)) except OSError: print('*** not found ***') if __name__ == '__main__': test() slixmpp/itests/test_basic_connect_and_message.py000066400000000000000000000015511477105560000226250ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestConnect(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) await self.connect_clients() async def test_send_message(self): """Make sure we can send and receive messages""" msg = self.clients[0].make_message( mto=self.clients[1].boundjid, mbody='Msg body', ) msg.send() message = await self.clients[1].wait_until('message') self.assertEqual(message['body'], msg['body']) suite = unittest.TestLoader().loadTestsFromTestCase(TestConnect) slixmpp/itests/test_blocking.py000066400000000000000000000021021477105560000172660ustar00rootroot00000000000000import unittest from slixmpp import JID from slixmpp.test.integration import SlixIntegration class TestBlocking(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0191']) await self.connect_clients() async def test_blocking(self): """Check we can block, unblock, and list blocked""" await self.clients[0]['xep_0191'].block( [JID('toto@example.com'), JID('titi@example.com')] ) blocked = {JID('toto@example.com'), JID('titi@example.com')} iq = await self.clients[0]['xep_0191'].get_blocked() self.assertEqual(iq['blocklist']['items'], blocked) info = await self.clients[0]['xep_0191'].unblock( blocked, ) iq = await self.clients[0]['xep_0191'].get_blocked() self.assertEqual(len(iq['blocklist']['items']), 0) suite = unittest.TestLoader().loadTestsFromTestCase(TestBlocking) slixmpp/itests/test_bob.py000066400000000000000000000017441477105560000162530ustar00rootroot00000000000000import asyncio import unittest from slixmpp.test.integration import SlixIntegration class TestBOB(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0231']) self.data = b'to' * 257 await self.connect_clients() async def test_bob(self): """Check we can send and receive a BOB.""" cid = await self.clients[0]['xep_0231'].set_bob( self.data, 'image/jpeg', ) recv = await self.clients[1]['xep_0231'].get_bob( jid=self.clients[0].boundjid, cid=cid, ) self.assertEqual(self.data, recv['bob']['data']) suite = unittest.TestLoader().loadTestsFromTestCase(TestBOB) slixmpp/itests/test_chatmarkers.py000066400000000000000000000015001477105560000200030ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestMarkers(SlixIntegration): async def asyncSetUp(self): self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0333']) await self.connect_clients() async def test_send_marker(self): """Send and receive a chat marker""" self.clients[0]['xep_0333'].send_marker( self.clients[1].boundjid.full, 'toto', 'displayed', ) msg = await self.clients[1].wait_until('marker_displayed') suite = unittest.TestLoader().loadTestsFromTestCase(TestMarkers) slixmpp/itests/test_disco.py000066400000000000000000000022241477105560000166040ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestDisco(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0030']) await self.connect_clients() async def test_features(self): """Check we can add, get and delete a feature""" self.clients[0]['xep_0030'].add_feature('urn:xmpp:fake:0') info = await self.clients[1]['xep_0030'].get_info( self.clients[0].boundjid.full ) self.assertIn('urn:xmpp:fake:0', info['disco_info']['features']) self.clients[0]['xep_0030'].del_feature(feature='urn:xmpp:fake:0') info = await self.clients[1]['xep_0030'].get_info( self.clients[0].boundjid.full ) self.assertNotIn('urn:xmpp:fake:0', info['disco_info']['features']) suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) slixmpp/itests/test_httpupload.py000066400000000000000000000022161477105560000176700ustar00rootroot00000000000000try: import aiohttp except ImportError: aiohttp = None import unittest from io import BytesIO from slixmpp.test.integration import SlixIntegration class TestHTTPUpload(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0363']) # Minimal data, we do not want to clutter the remote server self.data = b'tototo' await self.connect_clients() @unittest.skipIf(aiohttp is None, "aiohttp is not installed") async def test_httpupload(self): """Check we can upload a file properly.""" url = await self.clients[0]['xep_0363'].upload_file( 'toto.txt', input_file=BytesIO(self.data), size=len(self.data), ) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: text = await resp.text() self.assertEqual(text.encode('utf-8'), self.data) suite = unittest.TestLoader().loadTestsFromTestCase(TestHTTPUpload) slixmpp/itests/test_ibb.py000066400000000000000000000024011477105560000162340ustar00rootroot00000000000000import asyncio import unittest from slixmpp.test.integration import SlixIntegration class TestIBB(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) config = {'block_size': 256, 'auto_accept': True} self.register_plugins(['xep_0047'], [config]) self.data = b'to' * 257 await self.connect_clients() async def test_ibb(self): """Check we can send and receive data through ibb""" coro_in = self.clients[1].wait_until('ibb_stream_start') coro_out = self.clients[0]['xep_0047'].open_stream( self.clients[1].boundjid, sid='toto' ) instream, outstream = await asyncio.gather(coro_in, coro_out) async def send_and_close(): await outstream.sendall(self.data) await outstream.close() in_data, _ = await asyncio.gather(instream.gather(), send_and_close()) self.assertEqual(self.data, in_data) suite = unittest.TestLoader().loadTestsFromTestCase(TestIBB) slixmpp/itests/test_last_activity.py000066400000000000000000000021311477105560000203570ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestLastActivity(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0012']) await self.connect_clients() async def test_activity(self): """Check we can set and get last activity""" await self.clients[0]['xep_0012'].set_last_activity( status='coucou', seconds=4242, ) act = await self.clients[1]['xep_0012'].get_last_activity( self.clients[0].boundjid.full ) self.assertEqual(act['last_activity']['status'], 'coucou') self.assertGreater(act['last_activity']['seconds'], 4241) self.assertGreater(4250, act['last_activity']['seconds']) suite = unittest.TestLoader().loadTestsFromTestCase(TestLastActivity) slixmpp/itests/test_mam.py000066400000000000000000000055371477105560000162670ustar00rootroot00000000000000import unittest from random import randint from slixmpp import JID from slixmpp.test.integration import SlixIntegration class TestMAM(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0313']) await self.connect_clients() async def test_mam_retrieve(self): """Make sure we can get messages from our archive""" # send messages first tok = randint(1, 999999) self.clients[0].make_message( mto=self.clients[1].boundjid, mbody=f'coucou {tok}' ).send() await self.clients[1].wait_until('message') self.clients[1].make_message( mto=self.clients[0].boundjid, mbody=f'coucou coucou {tok}', ).send() await self.clients[0].wait_until('message') # Get archive retrieve = self.clients[0]['xep_0313'].retrieve( with_jid=JID(self.envjid('CI_ACCOUNT2')), iterator=True, reverse=True, rsm={'max': 2} ) msgs = [] count = 0 async for rsm in retrieve: for msg in rsm['mam']['results']: msgs.append( msg['mam_result']['forwarded']['stanza'] ) count += 1 if count >= 2: break self.assertEqual(msgs[0]['body'], f'coucou {tok}') self.assertEqual(msgs[1]['body'], f'coucou coucou {tok}') async def test_mam_iterate(self): """Make sure we can iterate over messages from our archive""" # send messages first tok = randint(1, 999999) self.clients[0].make_message( mto=self.clients[1].boundjid, mbody=f'coucou {tok}' ).send() await self.clients[1].wait_until('message') self.clients[1].make_message( mto=self.clients[0].boundjid, mbody='coucou coucou %s' % tok, ).send() await self.clients[0].wait_until('message') # Get archive retrieve = self.clients[0]['xep_0313'].iterate( with_jid=JID(self.envjid('CI_ACCOUNT2')), reverse=True, rsm={'max': 1} ) msgs = [] count = 0 async for msg in retrieve: msgs.append( msg['mam_result']['forwarded']['stanza'] ) count += 1 if count >= 2: break self.assertEqual(msgs[0]['body'], f'coucou coucou {tok}') self.assertEqual(msgs[1]['body'], f'coucou {tok}') suite = unittest.TestLoader().loadTestsFromTestCase(TestMAM) slixmpp/itests/test_moderate.py000066400000000000000000000046431477105560000173120ustar00rootroot00000000000000import asyncio import unittest import uuid from slixmpp import JID from slixmpp.test.integration import SlixIntegration UNIQUE = uuid.uuid4().hex class TestModerate(SlixIntegration): async def asyncSetUp(self): self.mucserver = self.envjid('CI_MUC_SERVER') self.muc = JID('%s@%s' % (UNIQUE, self.mucserver)) self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0425', 'xep_0359', 'xep_0045']) await self.connect_clients() async def setup_muc(self): self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) self.assertEqual(presence['muc']['affiliation'], 'owner') # Send initial configuration config = await self.clients[0]['xep_0045'].get_room_config(self.muc) values = config.get_values() values['muc#roomconfig_persistentroom'] = False values['muc#roomconfig_membersonly'] = True config['values'] = values config.reply() config = await self.clients[0]['xep_0045'].set_room_config(self.muc, config) # Send affiliation list including client 2 await self.clients[0]['xep_0045'].send_affiliation_list( self.muc, [ (self.clients[1].boundjid.bare, 'member'), ], ) self.clients[1]['xep_0045'].join_muc(self.muc, 'client2') await self.clients[1].wait_until('muc::%s::got_online' % self.muc) async def test_moderate_msg(self): """Try to moderate a message""" await self.setup_muc() msg = self.clients[1].make_message( mto=self.muc, mtype='groupchat', mbody='Coucou' ) msg.send() msg_recv = await self.clients[0].wait_until('groupchat_message') iqres, new_msg = await asyncio.gather( self.clients[0]['xep_0425'].moderate( self.muc, id=msg_recv['stanza_id']['id'], reason='Your message is bad.', ), self.clients[1].wait_until('moderated_message') ) self.assertTrue(new_msg['apply_to']['id'], msg_recv['id']) suite = unittest.TestLoader().loadTestsFromTestCase(TestModerate) slixmpp/itests/test_muc.py000066400000000000000000000061741477105560000162770ustar00rootroot00000000000000import asyncio import unittest from uuid import uuid4 from slixmpp import JID from slixmpp.test.integration import SlixIntegration UNIQUE = uuid4().hex class TestMUC(SlixIntegration): async def asyncSetUp(self): self.mucserver = self.envjid('CI_MUC_SERVER', default='chat.jabberfr.org') self.muc = JID('%s@%s' % (UNIQUE, self.mucserver)) self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0045']) await self.connect_clients() async def test_initial_join(self): """Check that we can connect to a new muc""" self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) self.assertEqual(presence['muc']['affiliation'], 'owner') async def test_setup_muc(self): """Check that sending the initial room config and affiliation list works""" self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) self.assertEqual(presence['muc']['affiliation'], 'owner') # Send initial configuration config = await self.clients[0]['xep_0045'].get_room_config(self.muc) values = config.get_values() values['muc#roomconfig_persistentroom'] = False values['muc#roomconfig_membersonly'] = True config['values'] = values config.reply() config = await self.clients[0]['xep_0045'].set_room_config(self.muc, config) # Send affiliation list including client 2 await self.clients[0]['xep_0045'].send_affiliation_list( self.muc, [ (self.clients[1].boundjid.bare, 'member'), ], ) async def test_join_after_config(self): """Join a room after being added to the affiliation list""" await self.test_setup_muc() self.clients[1]['xep_0045'].join_muc(self.muc, 'client2') await self.clients[1].wait_until('muc::%s::got_online' % self.muc) async def test_nick_change_leave(self): """Check that we change nicks and leave properly""" await self.test_join_after_config() nick = 'coucoucou2' new_nick = await self.clients[0]['xep_0045'].set_self_nick(self.muc, nick) assert new_nick == nick self.clients[0]['xep_0045'].leave_muc(self.muc, 'client1', 'boooring') pres = await self.clients[1].wait_until('muc::%s::got_offline' % self.muc) self.assertEqual(pres['status'], 'boooring') self.assertEqual(pres['type'], 'unavailable') async def test_kick(self): """Test kicking a user""" await self.test_join_after_config() await asyncio.gather( self.clients[0].wait_until('muc::%s::got_offline' % self.muc), self.clients[0]['xep_0045'].set_role(self.muc, 'client2', 'none') ) suite = unittest.TestLoader().loadTestsFromTestCase(TestMUC) slixmpp/itests/test_pep.py000066400000000000000000000046531477105560000162770ustar00rootroot00000000000000import asyncio import unittest from uuid import uuid4 from slixmpp.exceptions import IqError from slixmpp.test.integration import SlixIntegration from slixmpp.xmlstream import ElementBase, register_stanza_plugin from slixmpp.plugins.xep_0060.stanza import Item class Mystanza(ElementBase): namespace = 'random-ns' name = 'mystanza' plugin_attrib = 'mystanza' interfaces = {'test'} register_stanza_plugin(Item, Mystanza) class TestPEP(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0222', 'xep_0223']) for client in self.clients: client.auto_authorize = True await self.connect_clients() async def test_pep_public(self): """Check we can get and set public PEP data""" stanza = Mystanza() stanza['test'] = str(uuid4().hex) try: await self.clients[0]['xep_0060'].delete_node( self.clients[0].boundjid.bare, node=stanza.namespace, ) except: pass await self.clients[0]['xep_0222'].store(stanza, node=stanza.namespace, id='toto') fetched = await self.clients[0]['xep_0222'].retrieve( stanza.namespace, ) fetched_stanza = fetched['pubsub']['items']['item']['mystanza'] self.assertEqual(fetched_stanza['test'], stanza['test']) async def test_pep_private(self): """Check we can get and set private PEP data""" stanza = Mystanza() stanza['test'] = str(uuid4().hex) await self.clients[0]['xep_0223'].store( stanza, node='private-random', id='toto' ) fetched = await self.clients[0]['xep_0223'].retrieve( 'private-random', ) fetched_stanza = fetched['pubsub']['items']['item']['mystanza'] self.assertEqual(fetched_stanza['test'], stanza['test']) with self.assertRaises(IqError): fetched = await self.clients[1]['xep_0060'].get_item( jid=self.clients[0].boundjid.bare, node='private-random', item_id='toto', ) suite = unittest.TestLoader().loadTestsFromTestCase(TestPEP) slixmpp/itests/test_ping.py000066400000000000000000000011421477105560000164360ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestPing(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0199']) await self.connect_clients() async def test_ping(self): """Check we can ping our own server""" rtt = await self.clients[0]['xep_0199'].ping() self.assertGreater(10, rtt) suite = unittest.TestLoader().loadTestsFromTestCase(TestPing) slixmpp/itests/test_privatestorage.py000066400000000000000000000023011477105560000205360ustar00rootroot00000000000000import unittest from slixmpp import ET from slixmpp.test.integration import SlixIntegration from slixmpp.plugins.xep_0048.stanza import Bookmarks class TestPrivateStorage(SlixIntegration): async def asyncSetUp(self): self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0048', 'xep_0049']) await self.connect_clients() async def test_privatestorage(self): """Check we can set, get, and delete private in xml storage""" # Set a bookmark using private storage el = Bookmarks() el.add_conference('test@example.com', 'toto') await self.clients[0]['xep_0049'].store( el, ) result = await self.clients[0]['xep_0049'].retrieve('bookmarks') self.assertEqual(result['private']['bookmarks'], el) # Purge bookmarks await self.clients[0]['xep_0049'].store( Bookmarks(), ) result = await self.clients[0]['xep_0049'].retrieve('bookmarks') self.assertEqual(result['private']['bookmarks'], Bookmarks()) suite = unittest.TestLoader().loadTestsFromTestCase(TestPrivateStorage) slixmpp/itests/test_quickresponse.py000066400000000000000000000034231477105560000204000ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration from slixmpp.plugins.xep_0439 import stanza class TestQuickResponse(SlixIntegration): async def asyncSetUp(self): self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0439']) await self.connect_clients() async def test_quickresponse(self): """Send and receive actions and responses""" actions = [ ('id1', 'Action 1'), ('id2', 'Action 2'), ] self.clients[0]['xep_0439'].ask_for_actions( self.clients[1].boundjid.full, "Action 1 or 2 ?", actions ) msg = await self.clients[1].wait_until('action_received') actions_recv = [ (st['id'], st['label']) for st in msg if isinstance(st, stanza.Action) ] self.assertEqual( actions, actions_recv, ) reply = self.clients[1].make_message( mto=self.clients[0].boundjid.full ) reply['action_selected']['id'] = 'id1' reply.send() reply_recv = await self.clients[0].wait_until('action_selected') self.assertEqual( reply_recv['action_selected']['id'], 'id1', ) self.clients[0]['xep_0439'].ask_for_response( self.clients[1].boundjid.full, "Reply with action 1 or 2 (id1/id2) ?", actions ) msg = await self.clients[1].wait_until('responses_received') suite = unittest.TestLoader().loadTestsFromTestCase(TestQuickResponse) slixmpp/itests/test_reactions.py000066400000000000000000000020121477105560000174650ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestReactions(SlixIntegration): async def asyncSetUp(self): self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0444']) await self.connect_clients() async def test_send_reaction(self): """Make sure we can send and receive reactions""" self.clients[0]['xep_0444'].send_reactions( self.clients[1].boundjid.full, to_id='toto', reactions=['🦙', '🦦'], ) msg = await self.clients[1].wait_until('reactions') self.assertEqual( msg['reactions'].get_values(), {'🦙', '🦦'}, ) self.assertEqual(msg['reactions']['id'], 'toto') suite = unittest.TestLoader().loadTestsFromTestCase(TestReactions) slixmpp/itests/test_reconnect.py000066400000000000000000000014541477105560000174670ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestReconnect(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) await self.connect_clients() async def test_disconnect_connect(self): """Check we can disconnect and connect again""" await self.clients[0].disconnect() self.clients[0].connect() await self.clients[0].wait_until('session_start') async def test_reconnect(self): """Check we can reconnect()""" self.clients[0].reconnect() await self.clients[0].wait_until("session_start") suite = unittest.TestLoader().loadTestsFromTestCase(TestReconnect) slixmpp/itests/test_retract.py000066400000000000000000000016121477105560000171470ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestRetract(SlixIntegration): async def asyncSetUp(self): self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0424']) await self.connect_clients() async def test_retract_msg(self): """Try to retract a message""" self.clients[0]['xep_0424'].send_retraction( self.clients[1].boundjid.full, id='toto', fallback_text='Twas a mistake', ) msg = await self.clients[1].wait_until('message_retract') self.assertEqual(msg['retract']['id'], 'toto') suite = unittest.TestLoader().loadTestsFromTestCase(TestRetract) slixmpp/itests/test_selfping.py000066400000000000000000000061141477105560000173140ustar00rootroot00000000000000import asyncio import unittest from uuid import uuid4 from slixmpp import JID from slixmpp.test.integration import SlixIntegration from slixmpp.plugins.xep_0410 import PingStatus UNIQUE = uuid4().hex class TestSelfPing(SlixIntegration): async def asyncSetUp(self): self.mucserver = self.envjid('CI_MUC_SERVER', default='chat.jabberfr.org') self.muc = JID('%s@%s' % (UNIQUE, self.mucserver)) self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0410'], [{'ping_interval': 2}]) await self.connect_clients() async def config_room(self): self.clients[0]['xep_0045'].join_muc(self.muc, 'client1') presence = await self.clients[0].wait_until('muc::%s::got_online' % self.muc) config = await self.clients[0]['xep_0045'].get_room_config(self.muc) values = config.get_values() values['muc#roomconfig_persistentroom'] = False values['muc#roomconfig_membersonly'] = True config['values'] = values config.reply() config = await self.clients[0]['xep_0045'].set_room_config(self.muc, config) await self.clients[0]['xep_0045'].send_affiliation_list( self.muc, [ (self.clients[1].boundjid.bare, 'member'), ], ) async def test_presence_monitor(self): """Check that the ping status gets updated on room changes""" await self.config_room() self.clients[1]['xep_0045'].join_muc(self.muc, 'client2') full = JID(self.muc) full.resource = 'client2' self.clients[1]['xep_0410'].enable_self_ping(full) await self.clients[1].wait_until('muc::%s::got_online' % self.muc) await asyncio.sleep(3) self.assertEqual( self.clients[1]['xep_0410'].get_ping_status(full), PingStatus.JOINED, ) t = asyncio.create_task _, pending = await asyncio.wait( [ t(self.clients[0]['xep_0045'].set_role(self.muc, 'client2', 'none')), t(self.clients[0].wait_until('muc::%s::got_offline' % self.muc)), t(self.clients[1].wait_until('muc_ping_changed')), ], timeout=10, ) self.assertEqual(pending, set()) self.assertEqual( self.clients[1]['xep_0410'].get_ping_status(full), PingStatus.DISCONNECTED, ) self.clients[1]['xep_0045'].join_muc(self.muc, 'client2') await asyncio.wait( [ t(self.clients[1].wait_until('muc::%s::got_online' % self.muc)), t(self.clients[1].wait_until('muc_ping_changed')), t(asyncio.sleep(3))], timeout=10, ) self.assertEqual( self.clients[1]['xep_0410'].get_ping_status(full), PingStatus.JOINED, ) suite = unittest.TestLoader().loadTestsFromTestCase(TestSelfPing) slixmpp/itests/test_slow_filters.py000066400000000000000000000030631477105560000202210ustar00rootroot00000000000000import asyncio import unittest from slixmpp.test.integration import SlixIntegration from slixmpp import Message class TestSlowFilter(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) await self.connect_clients() async def test_filters(self): """Make sure filters work""" def add_a(stanza): if isinstance(stanza, Message): stanza['body'] = stanza['body'] + ' a' return stanza async def add_b(stanza): if isinstance(stanza, Message): stanza['body'] = stanza['body'] + ' b' return stanza async def add_c_wait(stanza): if isinstance(stanza, Message): await asyncio.sleep(2) stanza['body'] = stanza['body'] + ' c' return stanza self.clients[0].add_filter('out', add_a) self.clients[0].add_filter('out', add_b) self.clients[0].add_filter('out', add_c_wait) body = 'Msg body' msg = self.clients[0].make_message( mto=self.clients[1].boundjid, mbody=body, ) msg.send() message = await self.clients[1].wait_until('message') self.assertEqual(message['body'], body + ' a b c') suite = unittest.TestLoader().loadTestsFromTestCase(TestSlowFilter) slixmpp/itests/test_tls_check.py000066400000000000000000000105231477105560000174430ustar00rootroot00000000000000import asyncio import ssl import unittest from slixmpp import ClientXMPP from slixmpp.test.integration import SlixIntegration class TestTLS(SlixIntegration): async def test_connect_direct_tls(self): """Check that we can force connection in direct TLS""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) client.enable_direct_tls = True client.enable_starttls = False _, pending = await asyncio.wait( [asyncio.ensure_future(client.connect()), asyncio.ensure_future(client.wait_until('session_start'))], timeout=10 ) self.assertFalse(bool(pending)) extra_info = client.transport.get_extra_info('peername') self.assertEqual(extra_info[1], 5223) self.assertTrue(isinstance(client.socket, ssl.SSLObject)) async def test_connect_starttls(self): """Check that we can force connection in starttls""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) client.enable_direct_tls = False client.enable_starttls = True _, pending = await asyncio.wait( [asyncio.ensure_future(client.connect()), asyncio.ensure_future(client.wait_until('session_start'))], timeout=10 ) self.assertFalse(bool(pending)) extra_info = client.transport.get_extra_info('peername') self.assertEqual(extra_info[1], 5222) self.assertTrue(isinstance(client.socket, ssl.SSLObject)) async def test_connect_custom(self): """Check that we can connect with custom params""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) client.enable_direct_tls = False client.enable_starttls = True _, pending = await asyncio.wait( [asyncio.ensure_future(client.connect(host=self.envjid('CI_ACCOUNT1').host, port=5222)), asyncio.ensure_future(client.wait_until('session_start'))], timeout=10 ) self.assertFalse(bool(pending)) extra_info = client.transport.get_extra_info('peername') self.assertEqual(extra_info[1], 5222) self.assertTrue(isinstance(client.socket, ssl.SSLObject)) async def test_connect_custom_fail(self): """Check that providing bad custom params fail""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) client.enable_direct_tls = True client.enable_starttls = True fut = asyncio.Future() def handler(event): if not fut.done(): fut.set_result(True) with client.event_handler('connection_failed', handler): await client.connect(host=self.envjid('CI_ACCOUNT1').host, port=9999) res = await asyncio.wait([fut], timeout=1) self.assertTrue(res) async def test_connect_custom_fail2(self): """Check that providing bad custom params fail""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) client.enable_direct_tls = False client.enable_starttls = True fut = asyncio.Future() def handler(event): if not fut.done(): fut.set_result(True) with client.event_handler('connection_failed', handler): await client.connect(host=self.envjid('CI_ACCOUNT1').host, port=5223) res = await asyncio.wait([fut], timeout=1) self.assertTrue(res) async def test_validate_cert_custom(self): """Check that we can validate the cert manually""" client = ClientXMPP( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) def handler(cert): # Abort! client.abort() with client.event_handler('ssl_cert', handler): _, pending = await asyncio.wait( [asyncio.ensure_future(client.connect()), asyncio.ensure_future(client.wait_until('killed'))], timeout=10 ) self.assertFalse(bool(pending)) suite = unittest.TestLoader().loadTestsFromTestCase(TestTLS) slixmpp/itests/test_user_avatar.py000066400000000000000000000041171477105560000200220ustar00rootroot00000000000000import asyncio import unittest from slixmpp import JID from slixmpp.test.integration import SlixIntegration class TestUserAvatar(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0084', 'xep_0115']) self.data = b'coucou coucou' await self.connect_clients() await asyncio.gather( self.clients[0]['xep_0115'].update_caps(), ) async def _clear_avatar(self): """Utility for purging remote state""" await self.clients[0]['xep_0084'].stop() await self.clients[0]['xep_0084'].publish_avatar(b'') async def test_set_avatar(self): """Check we can set and get a PEP avatar and metadata""" await self._clear_avatar() await self.clients[0]['xep_0084'].publish_avatar( self.data ) metadata = { 'id': self.clients[0]['xep_0084'].generate_id(self.data), 'bytes': 13, 'type': 'image/jpeg', } # Wait for metadata publish event event = self.clients[0].wait_until('avatar_metadata_publish') publish = self.clients[0]['xep_0084'].publish_avatar_metadata( metadata, ) res = await asyncio.gather( event, publish, ) message = res[0] recv_meta = message['pubsub_event']['items']['item']['avatar_metadata'] info = recv_meta['info'] self.assertEqual(info['bytes'], metadata['bytes']) self.assertEqual(info['type'], metadata['type']) self.assertEqual(info['id'], metadata['id']) recv = await self.clients[0]['xep_0084'].retrieve_avatar( JID(self.clients[0].boundjid.bare), info['id'] ) avatar = recv['pubsub']['items']['item']['avatar_data']['value'] self.assertEqual(avatar, self.data) await self._clear_avatar() suite = unittest.TestLoader().loadTestsFromTestCase(TestUserAvatar) slixmpp/itests/test_vcard.py000066400000000000000000000027131477105560000166050ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestVcardTemp(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins(['xep_0054']) await self.connect_clients() async def _clear_vcard(self): # cleanup await self.clients[0]['xep_0054'].publish_vcard( self.clients[0]['xep_0054'].make_vcard() ) async def test_vcard(self): """Check we can set and get a vcard""" await self._clear_vcard() # Check that vcard is empty recv = await self.clients[1]['xep_0054'].get_vcard( self.clients[0].boundjid.bare ) self.assertEqual(recv['vcard_temp']['TITLE'], None) vcard = self.clients[0]['xep_0054'].make_vcard() vcard['TITLE'] = 'Coucou coucou' await self.clients[0]['xep_0054'].publish_vcard( vcard, ) # recv = await self.clients[1]['xep_0054'].get_vcard( self.clients[0].boundjid.bare ) self.assertEqual(recv['vcard_temp']['TITLE'], 'Coucou coucou') await self._clear_vcard() suite = unittest.TestLoader().loadTestsFromTestCase(TestVcardTemp) slixmpp/itests/test_vcard_avatar.py000066400000000000000000000027661477105560000201530ustar00rootroot00000000000000import asyncio import unittest from slixmpp import JID from slixmpp.test.integration import SlixIntegration from hashlib import sha1 class TestVcardAvatar(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.register_plugins(['xep_0153']) self.data = b'coucou coucou' self.hashed_data = sha1(self.data).hexdigest() await self.connect_clients() async def _clear_avatar(self): """Utility for purging remote state""" await self.clients[0]['xep_0153'].set_avatar(avatar=b'') async def test_set_avatar(self): """Check we can set and get a PEP avatar and metadata""" await self._clear_avatar() event = self.clients[0].wait_until('vcard_avatar_update') update = self.clients[0]['xep_0153'].set_avatar( avatar=self.data ) result = await asyncio.gather( event, update, ) presence = result[0] hash = presence['vcard_temp_update']['photo'] self.assertEqual(hash, self.hashed_data) iq = await self.clients[0]['xep_0054'].get_vcard( JID(self.clients[0].boundjid.bare) ) photo = iq['vcard_temp']['PHOTO']['BINVAL'] self.assertEqual(photo, self.data) await self._clear_avatar() suite = unittest.TestLoader().loadTestsFromTestCase(TestVcardAvatar) slixmpp/itests/test_version.py000066400000000000000000000022271477105560000171730ustar00rootroot00000000000000import unittest from slixmpp.test.integration import SlixIntegration class TestVersion(SlixIntegration): async def asyncSetUp(self): await super().asyncSetUp() self.add_client( self.envjid('CI_ACCOUNT1'), self.envstr('CI_ACCOUNT1_PASSWORD'), ) self.add_client( self.envjid('CI_ACCOUNT2'), self.envstr('CI_ACCOUNT2_PASSWORD'), ) self.register_plugins( ['xep_0092'], configs=[{ 'software_name': 'Slix Test', 'version': '1.2.3.4', 'os': 'I use arch btw', }] ) await self.connect_clients() async def test_version(self): """Check we can set and query software version info""" iq = await self.clients[1]['xep_0092'].get_version( self.clients[0].boundjid.full ) version = iq['software_version'] self.assertEqual(version['name'], 'Slix Test') self.assertEqual(version['version'], '1.2.3.4') self.assertEqual(version['os'], 'I use arch btw') suite = unittest.TestLoader().loadTestsFromTestCase(TestVersion) slixmpp/mypy.ini000066400000000000000000000003771477105560000142650ustar00rootroot00000000000000[mypy] check_untyped_defs = False ignore_missing_imports = True [mypy-slixmpp.types] ignore_errors = True [mypy-slixmpp.thirdparty.*] ignore_errors = True [mypy-slixmpp.plugins.*] ignore_errors = True [mypy-slixmpp.plugins.base] ignore_errors = False slixmpp/pyproject.toml000066400000000000000000000033231477105560000154740ustar00rootroot00000000000000[project] name = "slixmpp" version = "1.10.0" description = "Slixmpp is an elegant Python library for XMPP (aka Jabber)." readme = "README.rst" authors = [ { name = "Florent Le Coz", email = "louiz@louiz.org" }, { name = "Mathieu Pasquet (mathieui)", email = "mathieui@mathieui.net" }, { name = "Emmanuel Gil Peyrot (Link Mauve)", email = "linkmauve@linkmauve.fr" }, { name = "Maxime “pep” Buquet", email = "pep@bouah.org" }, ] maintainers = [ { name = "Mathieu Pasquet (mathieui)", email = "mathieui@mathieui.net" }, { name = "Emmanuel Gil Peyrot (Link Mauve)", email = "linkmauve@linkmauve.fr" }, { name = "Maxime “pep” Buquet", email = "pep@bouah.org" }, ] requires-python = ">=3.9" dependencies = [ "aiodns>=3.2.0", "pyasn1>=0.6.1", "pyasn1-modules>=0.4.1", ] classifiers = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Topic :: Internet :: XMPP', 'Topic :: Software Development :: Libraries :: Python Modules', ] [project.optional-dependencies] xep-0363 = ["aiohttp"] xep-0444-compliance = ["emoji"] xep-0454 = ["cryptography"] safer-xml-parsing = ["defusedxml"] [tool.maturin] module-name = "slixmpp.jid" python-packages = ["slixmpp"] python-source = "." [tool.uv] cache-keys = [{file = "pyproject.toml"}, {file = "rust/Cargo.toml"}, {file = "**/*.rs"}] [build-system] requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [dependency-groups] dev = ["mypy>=1.14.1"] slixmpp/run_integration_tests.py000077500000000000000000000033341477105560000175700ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import logging import unittest from argparse import ArgumentParser from importlib import import_module from pathlib import Path def run_tests(filenames=None, debug=False): """ Find and run all tests in the tests/ directory. Excludes live tests (tests/live_*). """ if sys.version_info < (3, 8): raise ValueError('Your python version is too old to run these tests') if not filenames: filenames = [i for i in Path('itests').glob('test_*')] else: filenames = [Path(i) for i in filenames] modules = ['.'.join(test.parts[:-1] + (test.stem,)) for test in filenames] suites = [] for filename in modules: module = import_module(filename) suites.append(module.suite) tests = unittest.TestSuite(suites) runner = unittest.TextTestRunner(verbosity=2) if debug: logging.basicConfig(level='DEBUG') else: # Disable logging output logging.basicConfig(level=100) logging.disable(100) result = runner.run(tests) return result if __name__ == '__main__': parser = ArgumentParser(description='Run unit tests.') parser.add_argument('tests', metavar='TEST', nargs='*', help='list of tests to run, or nothing to run them all') parser.add_argument('-d', '--debug', action='store_true', dest='debug', default=False, help='enable debug output') args = parser.parse_args() result = run_tests(args.tests, args.debug) print("" % ( "xmlns='http//andyet.net/protocol/tests'", result.testsRun, len(result.errors), len(result.failures), result.wasSuccessful())) sys.exit(not result.wasSuccessful()) slixmpp/run_tests.py000077500000000000000000000031531477105560000151640ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import logging import unittest from argparse import ArgumentParser from importlib import import_module from pathlib import Path def run_tests(filenames=None, debug=False): """ Find and run all tests in the tests/ directory. Excludes live tests (tests/live_*). """ if not filenames: filenames = [i for i in Path('tests').glob('test_*')] else: filenames = [Path(i) for i in filenames] modules = ['.'.join(test.parts[:-1] + (test.stem,)) for test in filenames] suites = [] for filename in modules: module = import_module(filename) suites.append(module.suite) tests = unittest.TestSuite(suites) runner = unittest.TextTestRunner(verbosity=2) if debug: logging.basicConfig(level='DEBUG') else: # Disable logging output logging.basicConfig(level=100) logging.disable(100) result = runner.run(tests) return result if __name__ == '__main__': parser = ArgumentParser(description='Run unit tests.') parser.add_argument('tests', metavar='TEST', nargs='*', help='list of tests to run, or nothing to run them all') parser.add_argument('-d', '--debug', action='store_true', dest='debug', default=False, help='enable debug output') args = parser.parse_args() result = run_tests(args.tests, args.debug) print("" % ( "xmlns='http//andyet.net/protocol/tests'", result.testsRun, len(result.errors), len(result.failures), result.wasSuccessful())) sys.exit(not result.wasSuccessful()) slixmpp/slixmpp/000077500000000000000000000000001477105560000142535ustar00rootroot00000000000000slixmpp/slixmpp/__init__.py000066400000000000000000000022701477105560000163650ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from os import getenv # Use defusedxml if wanted # Since enabling it can have adverse consequences for the programs using # slixmpp, do not enable it by default. if getenv('SLIXMPP_ENABLE_DEFUSEDXML', default='false').lower() == 'true': try: import defusedxml defusedxml.defuse_stdlib() except ImportError: pass from slixmpp.stanza import Message, Presence, Iq from slixmpp.jid import JID, InvalidJID from slixmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin from slixmpp.xmlstream.handler import * from slixmpp.xmlstream import XMLStream from slixmpp.xmlstream.matcher import * from slixmpp.basexmpp import BaseXMPP from slixmpp.clientxmpp import ClientXMPP from slixmpp.componentxmpp import ComponentXMPP from slixmpp.version import __version__, __version_info__ __all__ = [ 'Message', 'Presence', 'Iq', 'JID', 'InvalidJID', 'ET', 'ElementBase', 'register_stanza_plugin', 'XMLStream', 'BaseXMPP', 'ClientXMPP', 'ComponentXMPP', '__version__', '__version_info__' ] slixmpp/slixmpp/api.py000066400000000000000000000212721477105560000154020ustar00rootroot00000000000000from typing import Any, Optional, Callable from asyncio import iscoroutinefunction, Future from slixmpp.xmlstream import JID APIHandler = Callable[ [Optional[JID], Optional[str], Optional[JID], Any], Any ] class APIWrapper(object): """Slixmpp API wrapper. This class provide a shortened binding to access ``self.api`` from plugins without having to specify the plugin name or the global :class:`~.APIRegistry`. """ def __init__(self, api, name): self.api = api self.name = name if name not in self.api.settings: self.api.settings[name] = {} def __getattr__(self, attr: str): """Curry API management commands with the API name.""" if attr == 'name': return self.name elif attr == 'settings': return self.api.settings[self.name] elif attr == 'register': def partial(handler, op, jid=None, node=None, default=False): register = getattr(self.api, attr) return register(handler, self.name, op, jid, node, default) return partial elif attr == 'register_default': def partial1(handler, op, jid=None, node=None): return getattr(self.api, attr)(handler, self.name, op) return partial1 elif attr in ('run', 'restore_default', 'unregister'): def partial2(*args, **kwargs): return getattr(self.api, attr)(self.name, *args, **kwargs) return partial2 return None def __getitem__(self, attr): def partial(jid=None, node=None, ifrom=None, args=None): return self.api.run(self.name, attr, jid, node, ifrom, args) return partial class APIRegistry(object): """API Registry. This class is the global Slixmpp API registry, on which any handler will be registed. """ def __init__(self, xmpp): self._handlers = {} self._handler_defaults = {} self.xmpp = xmpp self.settings = {} def _setup(self, ctype: str, op: str): """Initialize the API callback dictionaries. :param ctype: The name of the API to initialize. :param op: The API operation to initialize. """ if ctype not in self.settings: self.settings[ctype] = {} if ctype not in self._handler_defaults: self._handler_defaults[ctype] = {} if ctype not in self._handlers: self._handlers[ctype] = {} if op not in self._handlers[ctype]: self._handlers[ctype][op] = {'global': None, 'jid': {}, 'node': {}} def wrap(self, ctype: str) -> APIWrapper: """Return a wrapper object that targets a specific API.""" return APIWrapper(self, ctype) def purge(self, ctype: str) -> None: """Remove all information for a given API.""" del self.settings[ctype] del self._handler_defaults[ctype] del self._handlers[ctype] def run(self, ctype: str, op: str, jid: Optional[JID] = None, node: Optional[str] = None, ifrom: Optional[JID] = None, args: Any = None) -> Future: """Execute an API callback, based on specificity. The API callback that is executed is chosen based on the combination of the provided JID and node: ====== ======= =================== JID node Handler ====== ======= =================== Given Given Node + JID handler Given None JID handler None Given Node handler None None Global handler ====== ======= =================== A node handler is responsible for servicing a single node at a single JID, while a JID handler may respond for any node at a given JID, and the global handler will answer to any JID+node combination. Handlers should check that the JID ``ifrom`` is authorized to perform the desired action. .. versionchanged:: 1.8.0 ``run()`` always returns a future, if the handler is a coroutine the future should be awaited on. :param ctype: The name of the API to use. :param op: The API operation to perform. :param jid: Optionally provide specific JID. :param node: Optionally provide specific node. :param ifrom: Optionally provide the requesting JID. :param args: Optional arguments to the handler. """ self._setup(ctype, op) if not jid: jid = self.xmpp.boundjid elif jid and not isinstance(jid, JID): jid = JID(jid) elif jid == JID(''): jid = self.xmpp.boundjid assert jid is not None if node is None: node = '' if self.xmpp.is_component: if self.settings[ctype].get('component_bare', False): jid_str = jid.bare else: jid_str = jid.full else: if self.settings[ctype].get('client_bare', False): jid_str = jid.bare else: jid_str = jid.full jid = JID(jid_str) handler = self._handlers[ctype][op]['node'].get((jid, node), None) if handler is None: handler = self._handlers[ctype][op]['jid'].get(jid, None) if handler is None: handler = self._handlers[ctype][op].get('global', None) if handler: try: if iscoroutinefunction(handler): return self.xmpp.wrap(handler(jid, node, ifrom, args)) else: future: Future = Future() result = handler(jid, node, ifrom, args) future.set_result(result) return future except TypeError: # To preserve backward compatibility, drop the ifrom # parameter for existing handlers that don't understand it. return handler(jid, node, args) future = Future() future.set_result(None) return future def register(self, handler: Optional[APIHandler], ctype: str, op: str, jid: Optional[JID] = None, node: Optional[str] = None, default: bool = False): """Register an API callback, with JID+node specificity. The API callback can later be executed based on the specificity of the provided JID+node combination. See :meth:`~.APIRegistry.run` for more details. :param ctype: The name of the API to use. :param op: The API operation to perform. :param jid: Optionally provide specific JID. :param node: Optionally provide specific node. """ self._setup(ctype, op) if jid is None and node is None: if handler is None: handler = self._handler_defaults[op] self._handlers[ctype][op]['global'] = handler elif jid is not None and node is None: self._handlers[ctype][op]['jid'][jid] = handler else: self._handlers[ctype][op]['node'][(jid, node)] = handler if default: self.register_default(handler, ctype, op) def register_default(self, handler, ctype: str, op: str): """Register a default, global handler for an operation. :param handler: The default, global handler for the operation. :param ctype: The name of the API to modify. :param op: The API operation to use. """ self._setup(ctype, op) self._handler_defaults[ctype][op] = handler def unregister(self, ctype: str, op: str, jid: Optional[JID] = None, node: Optional[str] = None): """Remove an API callback. The API callback chosen for removal is based on the specificity of the provided JID+node combination. See :meth:`~ApiRegistry.run` for more details. :param ctype: The name of the API to use. :param op: The API operation to perform. :param jid: Optionally provide specific JID. :param node: Optionally provide specific node. """ self._setup(ctype, op) self.register(None, ctype, op, jid, node) def restore_default(self, ctype: str, op: str, jid: Optional[JID] = None, node: Optional[str] = None): """Reset an API callback to use a default handler. :param ctype: The name of the API to use. :param op: The API operation to perform. :param jid: Optionally provide specific JID. :param node: Optionally provide specific node. """ self.unregister(ctype, op, jid, node) self.register(self._handler_defaults[ctype][op], ctype, op, jid, node) slixmpp/slixmpp/basexmpp.py000066400000000000000000000757771477105560000164730ustar00rootroot00000000000000# slixmpp.basexmpp # ~~~~~~~~~~~~~~~~~~ # This module provides the common XMPP functionality # for both clients and components. # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details from __future__ import annotations import asyncio import logging from typing import ( Dict, Optional, Union, TYPE_CHECKING, ) from slixmpp import plugins, roster, stanza from slixmpp.api import APIRegistry from slixmpp.exceptions import IqError, IqTimeout from slixmpp.stanza import ( Message, Presence, Iq, StreamError, ) from slixmpp.stanza.roster import Roster from slixmpp.xmlstream import XMLStream, JID from slixmpp.xmlstream import ET, register_stanza_plugin from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.stanzabase import ( ElementBase, XML_NS, ) from slixmpp.plugins import PluginManager, load_plugin, BasePlugin log = logging.getLogger(__name__) from slixmpp.types import ( PresenceTypes, MessageTypes, IqTypes, JidStr, OptJidStr, ) if TYPE_CHECKING: # Circular imports from slixmpp.pluginsdict import PluginsDict class BaseXMPP(XMLStream): """ The BaseXMPP class adapts the generic XMLStream class for use with XMPP. It also provides a plugin mechanism to easily extend and add support for new XMPP features. :param default_ns: Ensure that the correct default XML namespace is used during initialization. """ # This is technically not correct, but much more useful to typecheck # than the internal use of the PluginManager API plugin: PluginsDict def __init__(self, jid='', default_ns='jabber:client', **kwargs): XMLStream.__init__(self, **kwargs) self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' self.namespace_map[self.stream_ns] = 'stream' #: An identifier for the stream as given by the server. self.stream_id = None #: The JabberID (JID) requested for this connection. self.requested_jid = JID(jid) #: The JabberID (JID) used by this connection, #: as set after session binding. This may even be a #: different bare JID than what was requested. self.boundjid = JID(jid) self._expected_server_name = self.boundjid.host self._redirect_attempts = 0 #: The maximum number of consecutive see-other-host #: redirections that will be followed before quitting. self.max_redirects = 5 self.session_bind_event = asyncio.Event() #: A dictionary mapping plugin names to plugins. self.plugin = PluginManager(self) #: Configuration options for whitelisted plugins. #: If a plugin is registered without any configuration, #: and there is an entry here, it will be used. self.plugin_config = {} #: A list of plugins that will be loaded if #: :meth:`register_plugins` is called. self.plugin_whitelist = [] #: The main roster object. This roster supports multiple #: owner JIDs, as in the case for components. For clients #: which only have a single JID, see :attr:`client_roster`. self.roster = roster.Roster(self) self.roster.add(self.boundjid) #: The single roster for the bound JID. This is the #: equivalent of:: #: #: self.roster[self.boundjid.bare] self.client_roster = self.roster[self.boundjid] #: The distinction between clients and components can be #: important, primarily for choosing how to handle the #: ``'to'`` and ``'from'`` JIDs of stanzas. self.is_component = False #: Messages may optionally be tagged with ID values. Setting #: :attr:`use_message_ids` to `True` will assign all outgoing #: messages an ID. Some plugin features require enabling #: this option. self.use_message_ids = True #: Presence updates may optionally be tagged with ID values. #: Setting :attr:`use_message_ids` to `True` will assign all #: outgoing messages an ID. self.use_presence_ids = True #: XEP-0359 tag that gets added to stanzas. self.use_origin_id = False #: The API registry is a way to process callbacks based on #: JID+node combinations. Each callback in the registry is #: marked with: #: #: - An API name, e.g. xep_0030 #: - The name of an action, e.g. get_info #: - The JID that will be affected #: - The node that will be affected #: #: API handlers with no JID or node will act as global handlers, #: while those with a JID and no node will service all nodes #: for a JID, and handlers with both a JID and node will be #: used only for that specific combination. The handler that #: provides the most specificity will be used. self.api = APIRegistry(self) #: Flag indicating that the initial presence broadcast has #: been sent. Until this happens, some servers may not #: behave as expected when sending stanzas. self.sentpresence = False #: A reference to :mod:`slixmpp.stanza` to make accessing #: stanza classes easier. self.stanza = stanza self.register_handler( Callback('IM', MatchXPath('{%s}message/{%s}body' % (self.default_ns, self.default_ns)), self._handle_message)) self.register_handler( Callback('IMError', MatchXPath('{%s}message/{%s}error' % (self.default_ns, self.default_ns)), self._handle_message_error)) self.register_handler( Callback('Presence', MatchXPath("{%s}presence" % self.default_ns), self._handle_presence)) self.register_handler( Callback('Stream Error', MatchXPath("{%s}error" % self.stream_ns), self._handle_stream_error)) self.add_event_handler('session_start', self._handle_session_start) self.add_event_handler('disconnected', self._handle_disconnected) self.add_event_handler('presence_available', self._handle_available) self.add_event_handler('presence_dnd', self._handle_available) self.add_event_handler('presence_xa', self._handle_available) self.add_event_handler('presence_chat', self._handle_available) self.add_event_handler('presence_away', self._handle_available) self.add_event_handler('presence_unavailable', self._handle_unavailable) self.add_event_handler('presence_subscribe', self._handle_subscribe) self.add_event_handler('presence_subscribed', self._handle_subscribed) self.add_event_handler('presence_unsubscribe', self._handle_unsubscribe) self.add_event_handler('presence_unsubscribed', self._handle_unsubscribed) self.add_event_handler('roster_subscription_request', self._handle_new_subscription) # Set up the XML stream with XMPP's root stanzas. self.register_stanza(Message) self.register_stanza(Iq) self.register_stanza(Presence) self.register_stanza(StreamError) # Initialize a few default stanza plugins. register_stanza_plugin(Iq, Roster) def start_stream_handler(self, xml): """Save the stream ID once the streams have been established. :param xml: The incoming stream's root element. """ self.stream_id = xml.get('id', '') self.stream_version = xml.get('version', '') self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None) if not self.is_component and not self.stream_version: log.warning('Legacy XMPP 0.9 protocol detected.') self.event('legacy_protocol') def init_plugins(self): for name in self.plugin: if not hasattr(self.plugin[name], 'post_inited'): if hasattr(self.plugin[name], 'post_init'): self.plugin[name].post_init() self.plugin[name].post_inited = True def register_plugin(self, plugin: str, pconfig: Optional[Dict] = None, module=None): """Register and configure a plugin for use in this stream. :param plugin: The name of the plugin class. Plugin names must be unique. :param pconfig: A dictionary of configuration data for the plugin. Defaults to an empty dictionary. :param module: Optional refence to the module containing the plugin class if using custom plugins. """ # Use the global plugin config cache, if applicable if not pconfig: pconfig = self.plugin_config.get(plugin, {}) if not self.plugin.registered(plugin): # type: ignore load_plugin(plugin, module) self.plugin.enable(plugin, pconfig) # type: ignore def register_plugins(self): """Register and initialize all built-in plugins. Optionally, the list of plugins loaded may be limited to those contained in :attr:`plugin_whitelist`. Plugin configurations stored in :attr:`plugin_config` will be used. """ if self.plugin_whitelist: plugin_list = self.plugin_whitelist else: plugin_list = plugins.PLUGINS for plugin in plugin_list: if plugin in plugins.PLUGINS: self.register_plugin(plugin) else: raise NameError("Plugin %s not in plugins.PLUGINS." % plugin) def __getitem__(self, key): """Return a plugin given its name, if it has been registered.""" if key in self.plugin: return self.plugin[key] else: log.warning("Plugin '%s' is not loaded.", key) return False def get(self, key: str, default: Optional[BasePlugin] = None): """Return a plugin given its name, if it has been registered.""" return self.plugin.get(key, default) def Message(self, *args, **kwargs) -> stanza.Message: """Create a Message stanza associated with this stream.""" msg = Message(self, *args, **kwargs) msg['lang'] = self.default_lang return msg def Iq(self, *args, **kwargs) -> stanza.Iq: """Create an Iq stanza associated with this stream.""" return Iq(self, *args, **kwargs) def Presence(self, *args, **kwargs) -> stanza.Presence: """Create a Presence stanza associated with this stream.""" pres = Presence(self, *args, **kwargs) pres['lang'] = self.default_lang return pres def make_iq(self, id: Optional[str] = None, ifrom: OptJidStr = None, ito: OptJidStr = None, itype: Optional[IqTypes] = None, iquery: Optional[str] = None) -> stanza.Iq: """Create a new :class:`~.Iq` stanza with a given Id and from JID. :param id: An ideally unique ID value for this stanza thread. :param ifrom: The from :class:`~.JID` to use for this stanza. :param ito: The destination :class:`~.JID` for this stanza. :param itype: The :class:`~.Iq`'s type, one of: ``'get'``, ``'set'``, ``'result'``, or ``'error'``. :param iquery: Optional namespace for adding a query element. """ iq = self.Iq() if id is not None: iq['id'] = str(id) iq['to'] = ito iq['from'] = ifrom iq['type'] = itype iq['query'] = iquery return iq def make_iq_get(self, queryxmlns: Optional[str] =None, ito: OptJidStr = None, ifrom: OptJidStr = None, iq: Optional[stanza.Iq] = None) -> stanza.Iq: """Create an :class:`~.Iq` stanza of type ``'get'``. Optionally, a query element may be added. :param queryxmlns: The namespace of the query to use. :param ito: The destination :class:`~.JID` for this stanza. :param ifrom: The ``'from'`` :class:`~.JID` to use for this stanza. :param iq: Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['type'] = 'get' iq['query'] = queryxmlns if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_result(self, id: Optional[str] = None, ito: OptJidStr = None, ifrom: OptJidStr = None, iq: Optional[stanza.Iq] = None) -> stanza.Iq: """ Create an :class:`~.Iq` stanza of type ``'result'`` with the given ID value. :param id: An ideally unique ID value. May use :meth:`new_id()`. :param ito: The destination :class:`~.JID` for this stanza. :param ifrom: The ``'from'`` :class:`~.JID` to use for this stanza. :param iq: Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() if id is None: id = self.new_id() iq['id'] = id iq['type'] = 'result' if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_set(self, sub: Optional[Union[ElementBase, ET.Element]] = None, ito: OptJidStr = None, ifrom: OptJidStr = None, iq: Optional[stanza.Iq] = None) -> stanza.Iq: """ Create an :class:`~.Iq` stanza of type ``'set'``. Optionally, a substanza may be given to use as the stanza's payload. :param sub: Either an :class:`~.ElementBase` stanza object or an :class:`~xml.etree.ElementTree.Element` XML object to use as the :class:`~.Iq`'s payload. :param ito: The destination :class:`~.JID` for this stanza. :param ifrom: The ``'from'`` :class:`~.JID` to use for this stanza. :param iq: Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['type'] = 'set' if sub is not None: iq.append(sub) if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_error(self, id, type='cancel', condition='feature-not-implemented', text=None, ito=None, ifrom=None, iq=None): """ Create an :class:`~.Iq` stanza of type ``'error'``. :param id: An ideally unique ID value. May use :meth:`new_id()`. :param type: The type of the error, such as ``'cancel'`` or ``'modify'``. Defaults to ``'cancel'``. :param condition: The error condition. Defaults to ``'feature-not-implemented'``. :param text: A message describing the cause of the error. :param ito: The destination :class:`~.JID` for this stanza. :param ifrom: The ``'from'`` :class:`~jid.JID` to use for this stanza. :param iq: Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['id'] = id iq['error']['type'] = type iq['error']['condition'] = condition iq['error']['text'] = text if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_query(self, iq: Optional[stanza.Iq] = None, xmlns: str = '', ito: OptJidStr = None, ifrom: OptJidStr = None) -> stanza.Iq: """ Create or modify an :class:`~.Iq` stanza to use the given query namespace. :param iq: Optionally use an existing stanza instead of generating a new one. :param xmlns: The query's namespace. :param ito: The destination :class:`~.JID` for this stanza. :param ifrom: The ``'from'`` :class:`~.JID` to use for this stanza. """ if not iq: iq = self.Iq() iq['query'] = xmlns if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_query_roster(self, iq: Optional[stanza.Iq] = None) -> ET.Element: """Create a roster query element. :param iq: Optionally use an existing stanza instead of generating a new one. """ if iq: iq['query'] = 'jabber:iq:roster' return ET.Element("{jabber:iq:roster}query") def make_message(self, mto: JidStr, mbody: Optional[str] = None, msubject: Optional[str] = None, mtype: Optional[MessageTypes] = None, mhtml: Optional[str] = None, mfrom: OptJidStr = None, mnick: Optional[str] = None) -> stanza.Message: """ Create and initialize a new :class:`~.Message` stanza. :param mto: The recipient of the message. :param mbody: The main contents of the message. :param msubject: Optional subject for the message. :param mtype: The message's type, such as ``'chat'`` or ``'groupchat'``. :param mhtml: Optional HTML body content in the form of a string. :param mfrom: The sender of the message. if sending from a client, be aware that some servers require that the full JID of the sender be used. :param mnick: Optional nickname of the sender. """ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) message['body'] = mbody message['subject'] = msubject if mnick is not None: message['nick'] = mnick if mhtml is not None: message['html']['body'] = mhtml return message def make_presence(self, pshow: Optional[str] = None, pstatus: Optional[str] = None, ppriority: Optional[int] = None, pto: OptJidStr = None, ptype: Optional[PresenceTypes] = None, pfrom: OptJidStr = None, pnick: Optional[str] = None) -> stanza.Presence: """ Create and initialize a new :class:`~.Presence` stanza. :param pshow: The presence's show value. :param pstatus: The presence's status message. :param ppriority: This connection's priority. :param pto: The recipient of a directed presence. :param ptype: The type of presence, such as ``'subscribe'``. :param pfrom: The sender of the presence. :param pnick: Optional nickname of the presence's sender. """ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) if pshow is not None: presence['type'] = pshow if pfrom is None and self.is_component: presence['from'] = self.boundjid.full presence['priority'] = ppriority presence['status'] = pstatus presence['nick'] = pnick return presence def send_message(self, mto: JID, mbody: Optional[str] = None, msubject: Optional[str] = None, mtype: Optional[MessageTypes] = None, mhtml: Optional[str] = None, mfrom: OptJidStr = None, mnick: Optional[str] = None): """ Create, initialize, and send a new :class:`~.Message` stanza. :param mto: The recipient of the message. :param mbody: The main contents of the message. :param msubject: Optional subject for the message. :param mtype: The message's type, such as ``'chat'`` or ``'groupchat'``. :param mhtml: Optional HTML body content in the form of a string. :param mfrom: The sender of the message. if sending from a client, be aware that some servers require that the full JID of the sender be used. :param mnick: Optional nickname of the sender. """ self.make_message(mto, mbody, msubject, mtype, mhtml, mfrom, mnick).send() def send_presence(self, pshow: Optional[str] = None, pstatus: Optional[str] = None, ppriority: Optional[int] = None, pto: OptJidStr = None, ptype: Optional[PresenceTypes] = None, pfrom: OptJidStr = None, pnick: Optional[str] = None): """ Create, initialize, and send a new :class:`~.Presence` stanza. :param pshow: The presence's show value. :param pstatus: The presence's status message. :param ppriority: This connection's priority. :param pto: The recipient of a directed presence. :param ptype: The type of presence, such as ``'subscribe'``. :param pfrom: The sender of the presence. :param pnick: Optional nickname of the presence's sender. """ self.make_presence(pshow, pstatus, ppriority, pto, ptype, pfrom, pnick).send() def send_presence_subscription(self, pto: JidStr, pfrom: OptJidStr = None, ptype: PresenceTypes='subscribe', pnick: Optional[str] = None): """ Create, initialize, and send a new :class:`~.Presence` stanza of type ``'subscribe'``. :param pto: The recipient of a directed presence. :param pfrom: The sender of the presence. :param ptype: The type of presence, such as ``'subscribe'``. :param pnick: Optional nickname of the presence's sender. """ self.make_presence(ptype=ptype, pfrom=pfrom, pto=JID(pto).bare, pnick=pnick).send() @property def jid(self) -> str: """Attribute accessor for bare jid""" log.warning("jid property deprecated. Use boundjid.bare") return self.boundjid.bare @jid.setter def jid(self, value: str): log.warning("jid property deprecated. Use boundjid.bare") self.boundjid.bare = value @property def fulljid(self) -> str: """Attribute accessor for full jid""" log.warning("fulljid property deprecated. Use boundjid.full") return self.boundjid.full @fulljid.setter def fulljid(self, value: str): log.warning("fulljid property deprecated. Use boundjid.full") self.boundjid.full = value @property def resource(self) -> str: """Attribute accessor for jid resource""" log.warning("resource property deprecated. Use boundjid.resource") return self.boundjid.resource @resource.setter def resource(self, value: str): log.warning("fulljid property deprecated. Use boundjid.resource") self.boundjid.resource = value @property def username(self) -> str: """Attribute accessor for jid usernode""" log.warning("username property deprecated. Use boundjid.user") return self.boundjid.user @username.setter def username(self, value: str): log.warning("username property deprecated. Use boundjid.user") self.boundjid.user = value @property def server(self) -> str: """Attribute accessor for jid host""" log.warning("server property deprecated. Use boundjid.host") return self.boundjid.server @server.setter def server(self, value: str): log.warning("server property deprecated. Use boundjid.host") self.boundjid.server = value @property def auto_authorize(self) -> Optional[bool]: """Auto accept or deny subscription requests. If ``True``, auto accept subscription requests. If ``False``, auto deny subscription requests. If ``None``, don't automatically respond. """ return self.roster.auto_authorize @auto_authorize.setter def auto_authorize(self, value: Optional[bool]): self.roster.auto_authorize = value @property def auto_subscribe(self) -> bool: """Auto send requests for mutual subscriptions. If ``True``, auto send mutual subscription requests. """ return self.roster.auto_subscribe @auto_subscribe.setter def auto_subscribe(self, value: bool): self.roster.auto_subscribe = value def set_jid(self, jid: JidStr): """Rip a JID apart and claim it as our own.""" log.debug("setting jid to %s", jid) self.boundjid = JID(jid) def getjidresource(self, fulljid: str): if '/' in fulljid: return fulljid.split('/', 1)[-1] else: return '' def getjidbare(self, fulljid: str): return fulljid.split('/', 1)[0] def _handle_session_start(self, event): """Reset redirection attempt count.""" self._redirect_attempts = 0 def _handle_disconnected(self, event): """When disconnected, reset the roster""" self.roster.reset() self.session_bind_event.clear() def _handle_stream_error(self, error): self.event('stream_error', error) if error['condition'] == 'see-other-host': other_host = error['see_other_host'] if not other_host: log.warning("No other host specified.") return if self._redirect_attempts > self.max_redirects: log.error("Exceeded maximum number of redirection attempts.") return self._redirect_attempts += 1 host = other_host port = 5222 if '[' in other_host and ']' in other_host: host = other_host.split(']')[0][1:] elif ':' in other_host: host = other_host.split(':')[0] port_sec = other_host.split(']')[-1] if ':' in port_sec: port = int(port_sec.split(':')[1]) self.address = (host, port) self.default_domain = host self.dns_records = None self.reconnect() def _handle_message(self, msg): """Process incoming message stanzas.""" if not self.is_component and not msg['to'].bare: msg['to'] = self.boundjid self.event('message', msg) def _handle_message_error(self, msg): """Process incoming message error stanzas.""" if not self.is_component and not msg['to'].bare: msg['to'] = self.boundjid self.event('message_error', msg) def _handle_available(self, pres): self.roster[pres['to']][pres['from']].handle_available(pres) def _handle_unavailable(self, pres): self.roster[pres['to']][pres['from']].handle_unavailable(pres) def _handle_new_subscription(self, pres): """Attempt to automatically handle subscription requests. Subscriptions will be approved if the request is from a whitelisted JID, of :attr:`auto_authorize` is True. They will be rejected if :attr:`auto_authorize` is False. Setting :attr:`auto_authorize` to ``None`` will disable automatic subscription handling (except for whitelisted JIDs). If a subscription is accepted, a request for a mutual subscription will be sent if :attr:`auto_subscribe` is ``True``. """ roster = self.roster[pres['to']] item = self.roster[pres['to']][pres['from']] if item['whitelisted']: item.authorize() if roster.auto_subscribe: item.subscribe() elif roster.auto_authorize: item.authorize() if roster.auto_subscribe: item.subscribe() elif roster.auto_authorize == False: item.unauthorize() def _handle_removed_subscription(self, pres): self.roster[pres['to']][pres['from']].handle_unauthorize(pres) def _handle_subscribe(self, pres): self.roster[pres['to']][pres['from']].handle_subscribe(pres) def _handle_subscribed(self, pres): self.roster[pres['to']][pres['from']].handle_subscribed(pres) def _handle_unsubscribe(self, pres): self.roster[pres['to']][pres['from']].handle_unsubscribe(pres) def _handle_unsubscribed(self, pres): self.roster[pres['to']][pres['from']].handle_unsubscribed(pres) def _handle_presence(self, presence): """Process incoming presence stanzas. Update the roster with presence information. """ if self.roster[presence['from']].ignore_updates: return if not self.is_component and not presence['to'].bare: presence['to'] = self.boundjid self.event('presence', presence) self.event('presence_%s' % presence['type'], presence) # Check for changes in subscription state. if presence['type'] in ('subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'): self.event('changed_subscription', presence) return elif not presence['type'] in ('available', 'unavailable') and \ not presence['type'] in presence.showtypes: return def exception(self, exception): """Process any uncaught exceptions, notably :class:`~slixmpp.exceptions.IqError` and :class:`~slixmpp.exceptions.IqTimeout` exceptions. :param exception: An unhandled :class:`Exception` object. """ if isinstance(exception, IqError): iq = exception.iq log.error('%s: %s', iq['error']['condition'], iq['error']['text']) log.warning('You should catch IqError exceptions', exc_info=True) elif isinstance(exception, IqTimeout): iq = exception.iq log.error('Request timed out: %s', iq) log.warning('You should catch IqTimeout exceptions', exc_info=True) elif isinstance(exception, SyntaxError): # Hide stream parsing errors that occur when the # stream is disconnected (they've been handled, we # don't need to make a mess in the logs). pass else: log.exception(exception) slixmpp/slixmpp/clientxmpp.py000066400000000000000000000273371477105560000170240ustar00rootroot00000000000000 # slixmpp.clientxmpp # ~~~~~~~~~~~~~~~~~~~~ # This module provides XMPP functionality that # is specific to client connections. # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details import asyncio import logging from typing import Optional, Any, Callable, Tuple, Dict, Set, List from slixmpp.jid import JID from slixmpp.stanza import StreamFeatures, Iq from slixmpp.basexmpp import BaseXMPP from slixmpp.exceptions import XMPPError from slixmpp.roster.single import RosterNode from slixmpp.types import JidStr from slixmpp.xmlstream import XMLStream from slixmpp.xmlstream.stanzabase import StanzaBase from slixmpp.xmlstream.matcher import StanzaPath, MatchXPath from slixmpp.xmlstream.handler import Callback, CoroutineCallback log = logging.getLogger(__name__) class ClientXMPP(BaseXMPP): """ Slixmpp's client class. (Use only for good, not for evil.) Typical use pattern: .. code-block:: python xmpp = ClientXMPP('user@server.tld/resource', 'password') # ... Register plugins and event handlers ... xmpp.connect() asyncio.get_event_loop().run_forever() :param jid: The JID of the XMPP user account. :param password: The password for the XMPP user account. :param plugin_config: A dictionary of plugin configurations. :param plugin_whitelist: A list of approved plugins that will be loaded when calling :meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`. :param escape_quotes: **Deprecated.** """ client_roster: RosterNode def __init__(self, jid: JidStr, password: str, plugin_config=None, plugin_whitelist=None, escape_quotes=True, sasl_mech=None, lang='en', **kwargs): if not plugin_whitelist: plugin_whitelist = [] if not plugin_config: plugin_config = {} BaseXMPP.__init__(self, jid, 'jabber:client', **kwargs) self.escape_quotes = escape_quotes self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.default_port = 5222 self.default_domain = self.boundjid.host self.default_lang = lang self.credentials: Dict[str, str] = {} self.password = password self.stream_header = "" % ( self.boundjid.host, "xmlns:stream='%s'" % self.stream_ns, "xmlns='%s'" % self.default_ns, "xml:lang='%s'" % self.default_lang, "version='1.0'") self.stream_footer = "" self.features: Set[str] = set() self._stream_feature_handlers: Dict[str, Tuple[Callable, bool]] = {} self._stream_feature_order: List[Tuple[int, str]] = [] self.tls_services = {'xmpps-client'} self.starttls_services = {'xmpp-client'} #TODO: Use stream state here self.authenticated = False self.sessionstarted = False self.bound = False self.bindfail = False self.add_event_handler('connected', self._reset_connection_state) self.add_event_handler('session_bind', self._handle_session_bind) self.add_event_handler('roster_update', self._handle_roster) self.register_stanza(StreamFeatures) self.register_handler( CoroutineCallback( 'Stream Features', MatchXPath('{%s}features' % self.stream_ns), self._handle_stream_features, # type: ignore ) ) def roster_push_filter(iq: StanzaBase) -> None: from_ = iq['from'] if from_ and from_ != JID('') and from_ != self.boundjid.bare: reply = iq.reply() reply['type'] = 'error' reply['error']['type'] = 'cancel' reply['error']['code'] = 503 reply['error']['condition'] = 'service-unavailable' reply.send() return self.event('roster_update', iq) self.register_handler( Callback('Roster Update', StanzaPath('iq@type=set/roster'), roster_push_filter)) # Setup default stream features self.register_plugin('feature_starttls') self.register_plugin('feature_bind') self.register_plugin('feature_session') self.register_plugin('feature_rosterver') self.register_plugin('feature_preapproval') self.register_plugin('feature_mechanisms') if sasl_mech: self['feature_mechanisms'].use_mech = sasl_mech @property def password(self) -> str: return self.credentials.get('password', '') @password.setter def password(self, value: str) -> None: self.credentials['password'] = value def connect(self, host: Optional[str] = None, port: Optional[int] = None) -> asyncio.Future: """Connect to the XMPP server. When no address is given, a SRV lookup for the server will be attempted. If that fails, the server used in the JID will be used. :param host: A custom host to connect to (requires port as well) :param port: A custom port to connect to (requires host as well) """ # If an address was provided, disable using DNS SRV lookup; # otherwise, use the domain from the client JID with the standard # XMPP client port and allow SRV lookup. if not (host and port): host, port = None, None self.init_plugins() return XMLStream.connect(self, host=host, port=port) def register_feature(self, name: str, handler: Callable, restart: bool = False, order: int = 5000) -> None: """Register a stream feature handler. :param name: The name of the stream feature. :param handler: The function to execute if the feature is received. :param restart: Indicates if feature processing should halt with this feature. Defaults to ``False``. :param order: The relative ordering in which the feature should be negotiated. Lower values will be attempted earlier when available. """ self._stream_feature_handlers[name] = (handler, restart) self._stream_feature_order.append((order, name)) self._stream_feature_order.sort() def unregister_feature(self, name: str, order: int) -> None: if name in self._stream_feature_handlers: del self._stream_feature_handlers[name] self._stream_feature_order.remove((order, name)) self._stream_feature_order.sort() def update_roster(self, jid: JID, **kwargs) -> None: """Add or change a roster item. :param jid: The JID of the entry to modify. :param name: The user's nickname for this JID. :param subscription: The subscription status. May be one of ``'to'``, ``'from'``, ``'both'``, or ``'none'``. If set to ``'remove'``, the entry will be deleted. :param groups: The roster groups that contain this item. :param timeout: The length of time (in seconds) to wait for a response before continuing if blocking is used. Defaults to :attr:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`. :param callback: Optional reference to a stream handler function. Will be executed when the roster is received. Implies ``block=False``. """ current = self.client_roster[jid] name = kwargs.get('name', current['name']) subscription = kwargs.get('subscription', current['subscription']) groups = kwargs.get('groups', current['groups']) timeout = kwargs.get('timeout', None) callback = kwargs.get('callback', None) return self.client_roster.update(jid, name, subscription, groups, timeout, callback) def del_roster_item(self, jid): """Remove an item from the roster. This is done by setting its subscription status to ``'remove'``. :param jid: The JID of the item to remove. """ return self.client_roster.remove(jid) def get_roster(self, callback=None, timeout=None): """Request the roster from the server. :param callback: Reference to a stream handler function. Will be executed when the roster is received. """ iq = self.Iq() iq['type'] = 'get' iq.enable('roster') if 'rosterver' in self.features: iq['roster']['ver'] = self.client_roster.version if callback is None: callback = lambda resp: self.event('roster_update', resp) else: orig_cb = callback def wrapped(resp): self.event('roster_update', resp) orig_cb(resp) callback = wrapped return iq.send(callback, timeout) def _reset_connection_state(self, event: Optional[Any] = None) -> None: #TODO: Use stream state here self.authenticated = False self.sessionstarted = False self.bound = False self.bindfail = False self.features = set() async def _handle_stream_features(self, features: StreamFeatures) -> Optional[bool]: """Process the received stream features. :param features: The features stanza. """ for order, name in self._stream_feature_order: if name in features['features']: handler, restart = self._stream_feature_handlers[name] if asyncio.iscoroutinefunction(handler): result = await handler(features) else: result = handler(features) if result and restart: # Don't continue if the feature requires # restarting the XML stream. return True log.debug('Finished processing stream features.') self.event('stream_negotiated') return None def _handle_roster(self, iq: Iq) -> None: """Update the roster after receiving a roster stanza. :param iq: The roster stanza. """ if iq['type'] == 'set': if iq['from'].bare and iq['from'].bare != self.boundjid.bare: raise XMPPError(condition='service-unavailable') roster = self.client_roster if iq['roster']['ver']: roster.version = iq['roster']['ver'] items = iq['roster']['items'] valid_subscriptions = ('to', 'from', 'both', 'none', 'remove') for jid, item in items.items(): if item['subscription'] in valid_subscriptions: roster[jid]['name'] = item['name'] roster[jid]['groups'] = item['groups'] roster[jid]['from'] = item['subscription'] in ('from', 'both') roster[jid]['to'] = item['subscription'] in ('to', 'both') roster[jid]['pending_out'] = (item['ask'] == 'subscribe') roster[jid].save(remove=(item['subscription'] == 'remove')) if iq['type'] == 'set': resp = self.Iq(stype='result', sto=iq['from'], sid=iq['id']) resp.enable('roster') resp.send() def _handle_session_bind(self, jid: JID) -> None: """Set the client roster to the JID set by the server. :param :class:`slixmpp.xmlstream.jid.JID` jid: The bound JID as dictated by the server. The same as :attr:`boundjid`. """ self.client_roster = self.roster[jid] slixmpp/slixmpp/componentxmpp.py000066400000000000000000000130721477105560000175370ustar00rootroot00000000000000 # slixmpp.clientxmpp # ~~~~~~~~~~~~~~~~~~~~ # This module provides XMPP functionality that # is specific to external server component connections. # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details import logging import hashlib from asyncio import Future from typing import Optional from slixmpp import Message, Iq, Presence from slixmpp.basexmpp import BaseXMPP from slixmpp.stanza import Handshake from slixmpp.stanza.error import Error from slixmpp.xmlstream import XMLStream from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.stanzabase import register_stanza_plugin log = logging.getLogger(__name__) class ComponentXMPP(BaseXMPP): """ Slixmpp's basic XMPP server component. Use only for good, not for evil. :param jid: The JID of the component. :param secret: The secret or password for the component. :param host: The server accepting the component. :param port: The port used to connect to the server. :param plugin_config: A dictionary of plugin configurations. :param plugin_whitelist: A list of approved plugins that will be loaded when calling :meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`. :param use_jc_ns: Indicates if the ``'jabber:client'`` namespace should be used instead of the standard ``'jabber:component:accept'`` namespace. Defaults to ``False``. :param fix_error_ns: Fix the namespace of error stanzas. If you use ``use_jc_ns`` namespace, you probably want that, but it can be a problem if you use both a ClientXMPP and a ComponentXMPP in the same interpreter. This is ``False`` by default for backwards compatibility. """ def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False, fix_error_ns=False): if not plugin_whitelist: plugin_whitelist = [] if not plugin_config: plugin_config = {} if use_jc_ns: default_ns = 'jabber:client' else: default_ns = 'jabber:component:accept' BaseXMPP.__init__(self, jid, default_ns) if fix_error_ns: self._fix_error_ns() self.enable_starttls = False self.enable_plaintext = True self.auto_authorize = None self.stream_header = '' % ( 'xmlns="jabber:component:accept"', 'xmlns:stream="%s"' % self.stream_ns, jid) self.stream_footer = "" self.server_host = host self.default_domain = host self.server_port = port self.secret = secret self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.is_component = True self.sessionstarted = False self.register_handler( Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handle_handshake)) self.add_event_handler('presence_probe', self._handle_probe) def _fix_error_ns(self): Error.namespace = self.default_ns for st in Message, Iq, Presence: register_stanza_plugin(st, Error) def connect(self, host: Optional[str] = None, port: Optional[int] = None) -> Future: """Connect to the server. :param host: The name of the desired server for the connection. Defaults to :attr:`server_host`. :param port: Port to connect to on the server. Defauts to :attr:`server_port`. """ if host is not None: self.server_host = host if port is not None: self.server_port = port self.server_name = self.boundjid.host self.init_plugins() log.debug("Connecting to %s:%s", self.server_host, self.server_port) return XMLStream.connect(self, host=self.server_host, port=self.server_port) def incoming_filter(self, xml): """ Pre-process incoming XML stanzas by converting any ``'jabber:client'`` namespaced elements to the component's default namespace. :param xml: The XML stanza to pre-process. """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) return xml def start_stream_handler(self, xml): """ Once the streams are established, attempt to handshake with the server to be accepted as a component. :param xml: The incoming stream's root element. """ BaseXMPP.start_stream_handler(self, xml) # Construct a hash of the stream ID and the component secret. sid = xml.get('id', '') pre_hash = bytes('%s%s' % (sid, self.secret), 'utf-8') handshake = Handshake() handshake['value'] = hashlib.sha1(pre_hash).hexdigest().lower() self.send(handshake) def _handle_handshake(self, xml): """The handshake has been accepted. :param xml: The reply handshake stanza. """ self.session_bind_event.set() self.sessionstarted = True self.event('session_bind', self.boundjid) self.event('session_start') def _handle_probe(self, pres): self.roster[pres['to']][pres['from']].handle_probe(pres) slixmpp/slixmpp/exceptions.py000066400000000000000000000114641477105560000170140ustar00rootroot00000000000000 # slixmpp.exceptions # ~~~~~~~~~~~~~~~~~~~~ # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details from typing import Dict, Optional from .types import ErrorConditions, ErrorTypes, JidStr class XMPPError(Exception): """ A generic exception that may be raised while processing an XMPP stanza to indicate that an error response stanza should be sent. The exception method for stanza objects extending :class:`~slixmpp.stanza.rootstanza.RootStanza` will create an error stanza and initialize any additional substanzas using the extension information included in the exception. Meant for use in Slixmpp plugins and applications using Slixmpp. Extension information can be included to add additional XML elements to the generated error stanza. :param condition: The XMPP defined error condition. Defaults to ``'undefined-condition'``. :param text: Human readable text describing the error. :param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``. Defaults to ``'cancel'``. :param extension: Tag name of the extension's XML content. :param extension_ns: XML namespace of the extensions' XML content. :param extension_args: Content and attributes for the extension element. Same as the additional arguments to the :class:`~xml.etree.ElementTree.Element` constructor. :param clear: Indicates if the stanza's contents should be removed before replying with an error. Defaults to ``True``. """ def __init__(self, condition: ErrorConditions='undefined-condition', text='', etype: Optional[ErrorTypes]=None, extension=None, extension_ns=None, extension_args=None, clear=True, by: Optional[JidStr] = None): if extension_args is None: extension_args = {} if condition not in _DEFAULT_ERROR_TYPES: raise ValueError("This is not a valid condition type", condition) if etype is None: etype = _DEFAULT_ERROR_TYPES[condition] self.by = by self.condition = condition self.text = text self.etype = etype self.clear = clear self.extension = extension self.extension_ns = extension_ns self.extension_args = extension_args def format(self): """ Format the error in a simple user-readable string. """ text = [self.etype, self.condition] if self.text: text.append(self.text) if self.extension: text.append(self.extension) # TODO: handle self.extension_args return ': '.join(text) class IqTimeout(XMPPError): """ An exception which indicates that an IQ request response has not been received within the alloted time window. """ def __init__(self, iq): super().__init__( condition='remote-server-timeout', etype='cancel') #: The :class:`~slixmpp.stanza.iq.Iq` stanza whose response #: did not arrive before the timeout expired. self.iq = iq class IqError(XMPPError): """ An exception raised when an Iq stanza of type 'error' is received after making a blocking send call. """ def __init__(self, iq): super().__init__( condition=iq['error']['condition'], text=iq['error']['text'], etype=iq['error']['type']) #: The :class:`~slixmpp.stanza.iq.Iq` error result stanza. self.iq = iq class PresenceError(XMPPError): """ An exception raised in specific circumstances for presences of type 'error' received. """ def __init__(self, pres): super().__init__( condition=pres['error']['condition'], text=pres['error']['text'], etype=pres['error']['type'], ) self.presence = pres _DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = { "bad-request": "modify", "conflict": "cancel", "feature-not-implemented": "cancel", "forbidden": "auth", "gone": "modify", "internal-server-error": "wait", "item-not-found": "cancel", "jid-malformed": "modify", "not-acceptable": "modify", "not-allowed": "cancel", "not-authorized": "auth", "payment-required": "auth", "policy-violation": "modify", "recipient-unavailable": "wait", "redirect": "modify", "registration-required": "auth", "remote-server-not-found": "cancel", "remote-server-timeout": "wait", "resource-constraint": "wait", "service-unavailable": "cancel", "subscription-required": "auth", "undefined-condition": "cancel", "unexpected-request": "modify", } slixmpp/slixmpp/features/000077500000000000000000000000001477105560000160715ustar00rootroot00000000000000slixmpp/slixmpp/features/__init__.py000066400000000000000000000004711477105560000202040ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. __all__ = [ 'feature_starttls', 'feature_mechanisms', 'feature_bind', 'feature_session', 'feature_rosterver', 'feature_preapproval' ] slixmpp/slixmpp/features/feature_bind/000077500000000000000000000000001477105560000205205ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_bind/__init__.py000066400000000000000000000005351477105560000226340ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_bind.bind import FeatureBind from slixmpp.features.feature_bind.stanza import Bind register_plugin(FeatureBind) slixmpp/slixmpp/features/feature_bind/bind.py000066400000000000000000000037551477105560000220200ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from slixmpp.jid import JID from slixmpp.stanza import Iq, StreamFeatures from slixmpp.features.feature_bind import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from typing import ClassVar, Set log = logging.getLogger(__name__) class FeatureBind(BasePlugin): name = 'feature_bind' description = 'RFC 6120: Stream Feature: Resource Binding' dependencies: ClassVar[Set[str]] = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('bind', self._handle_bind_resource, restart=False, order=10000) register_stanza_plugin(Iq, stanza.Bind) register_stanza_plugin(StreamFeatures, stanza.Bind) async def _handle_bind_resource(self, features): """ Handle requesting a specific resource. Arguments: features -- The stream features stanza. """ log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource) self.features = features iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') if self.xmpp.requested_jid.resource: iq['bind']['resource'] = self.xmpp.requested_jid.resource await iq.send(callback=self._on_bind_response) def _on_bind_response(self, response): self.xmpp.boundjid = JID(response['bind']['jid']) self.xmpp.bound = True self.xmpp.event('session_bind', self.xmpp.boundjid) self.xmpp.session_bind_event.set() self.xmpp.features.add('bind') log.info("JID set to: %s", self.xmpp.boundjid.full) if 'session' not in self.features['features']: log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') slixmpp/slixmpp/features/feature_bind/stanza.py000066400000000000000000000006271477105560000223770ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class Bind(ElementBase): """ """ name = 'bind' namespace = 'urn:ietf:params:xml:ns:xmpp-bind' interfaces = {'resource', 'jid'} sub_interfaces = interfaces plugin_attrib = 'bind' slixmpp/slixmpp/features/feature_mechanisms/000077500000000000000000000000001477105560000217335ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_mechanisms/__init__.py000066400000000000000000000010731477105560000240450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms from slixmpp.features.feature_mechanisms.stanza import Mechanisms from slixmpp.features.feature_mechanisms.stanza import Auth from slixmpp.features.feature_mechanisms.stanza import Success from slixmpp.features.feature_mechanisms.stanza import Failure register_plugin(FeatureMechanisms) slixmpp/slixmpp/features/feature_mechanisms/mechanisms.py000066400000000000000000000234431477105560000244420ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import ssl import logging from slixmpp.util import sasl from slixmpp.util.stringprep_profiles import StringPrepError from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import Callback from slixmpp.features.feature_mechanisms import stanza from typing import ClassVar, Set log = logging.getLogger(__name__) class FeatureMechanisms(BasePlugin): name = 'feature_mechanisms' description = 'RFC 6120: Stream Feature: SASL' dependencies: ClassVar[Set[str]] = set() stanza = stanza default_config = { 'use_mech': None, 'use_mechs': None, 'min_mech': None, 'sasl_callback': None, 'security_callback': None, 'encrypted_plain': True, 'unencrypted_plain': False, 'unencrypted_digest': False, 'unencrypted_cram': False, 'unencrypted_scram': False, 'order': 100, 'tls_version': None, } def plugin_init(self): if self.sasl_callback is None: self.sasl_callback = self._default_credentials if self.security_callback is None: self.security_callback = self._default_security creds = self.sasl_callback({'username'}, set()) if not self.use_mech and not creds['username']: self.use_mech = 'ANONYMOUS' self.mech = None self.mech_list = set() self.attempted_mechs = set() register_stanza_plugin(StreamFeatures, stanza.Mechanisms) self.xmpp.register_stanza(stanza.Success) self.xmpp.register_stanza(stanza.Failure) self.xmpp.register_stanza(stanza.Auth) self.xmpp.register_stanza(stanza.Challenge) self.xmpp.register_stanza(stanza.Response) self.xmpp.register_stanza(stanza.Abort) self.xmpp.register_handler( Callback('SASL Success', MatchXPath(stanza.Success.tag_name()), self._handle_success, instream=True)) self.xmpp.register_handler( Callback('SASL Failure', MatchXPath(stanza.Failure.tag_name()), self._handle_fail, instream=True)) self.xmpp.register_handler( Callback('SASL Challenge', MatchXPath(stanza.Challenge.tag_name()), self._handle_challenge)) self.xmpp.register_feature('mechanisms', self._handle_sasl_auth, restart=True, order=self.order) def _default_credentials(self, required_values, optional_values): creds = self.xmpp.credentials result = {} values = required_values.union(optional_values) for value in values: if value == 'username': result[value] = creds.get('username', self.xmpp.requested_jid.user) elif value == 'email': jid = self.xmpp.requested_jid.bare result[value] = creds.get('email', jid) elif value == 'channel_binding': if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): version = self.xmpp.socket.version() # As of now, python does not implement anything else # than tls-unique, which is forbidden on TLSv1.3 # see https://github.com/python/cpython/issues/95341 if version != 'TLSv1.3': result[value] = self.xmpp.socket.get_channel_binding( cb_type="tls-unique" ) elif 'tls-exporter' in ssl.CHANNEL_BINDING_TYPES: result[value] = self.xmpp.socket.get_channel_binding( cb_type="tls-exporter" ) else: result[value] = None else: result[value] = None elif value == 'host': result[value] = creds.get('host', self.xmpp.requested_jid.domain) elif value == 'realm': result[value] = creds.get('realm', self.xmpp.requested_jid.domain) elif value == 'service-name': result[value] = creds.get('service-name', self.xmpp._service_name) elif value == 'service': result[value] = creds.get('service', 'xmpp') elif value in creds: result[value] = creds[value] return result def _default_security(self, values): result = {} for value in values: if value == 'encrypted': if 'starttls' in self.xmpp.features: result[value] = True elif isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): result[value] = True else: result[value] = False elif value == 'tls_version': if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): result[value] = self.xmpp.socket.version() elif value == 'binding_proposed': result[value] = any(x for x in self.mech_list if x.endswith('-PLUS')) else: result[value] = self.config.get(value, False) return result def _handle_sasl_auth(self, features): """ Handle authenticating using SASL. Arguments: features -- The stream features stanza. """ if 'mechanisms' in self.xmpp.features: # SASL authentication has already succeeded, but the # server has incorrectly offered it again. return False enforce_limit = False limited_mechs = self.use_mechs if limited_mechs is None: limited_mechs = set() elif limited_mechs and not isinstance(limited_mechs, set): limited_mechs = set(limited_mechs) enforce_limit = True if self.use_mech: limited_mechs.add(self.use_mech) enforce_limit = True if enforce_limit: self.use_mechs = limited_mechs self.mech_list = set(features['mechanisms']) return self._send_auth() def _send_auth(self): mech_list = self.mech_list - self.attempted_mechs try: self.mech = sasl.choose(mech_list, self.sasl_callback, self.security_callback, limit=self.use_mechs, min_mech=self.min_mech) except sasl.SASLNoAppropriateMechanism: log.error("No appropriate login method.") self.xmpp.event("failed_all_auth") if not self.attempted_mechs: # Only trigger this event if we didn't try at least one # method self.xmpp.event("no_auth") self.attempted_mechs = set() return self.xmpp.disconnect() except StringPrepError: log.exception("A credential value did not pass SASLprep.") self.xmpp.disconnect() resp = stanza.Auth(self.xmpp) resp['mechanism'] = self.mech.name try: resp['value'] = self.mech.process() except sasl.SASLCancelled: self.attempted_mechs.add(self.mech.name) self._send_auth() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() except sasl.SASLFailed: self.attempted_mechs.add(self.mech.name) self._send_auth() else: resp.send() return True def _handle_challenge(self, stanza): """SASL challenge received. Process and send response.""" resp = self.stanza.Response(self.xmpp) try: resp['value'] = self.mech.process(stanza['value']) except sasl.SASLCancelled: self.stanza.Abort(self.xmpp).send() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() except sasl.SASLFailed: self.stanza.Abort(self.xmpp).send() else: if resp.get_value() == '': resp.del_value() resp.send() def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" try: final = self.mech.process(stanza['value']) except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() else: self.attempted_mechs = set() self.xmpp.authenticated = True self.xmpp.features.add('mechanisms') self.xmpp.event('auth_success', stanza) # Restart the stream self.xmpp.init_parser() self.xmpp.send_raw(self.xmpp.stream_header) def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" self.attempted_mechs.add(self.mech.name) log.info("Authentication failed: %s", stanza['condition']) self.xmpp.event("failed_auth", stanza) self._send_auth() return True slixmpp/slixmpp/features/feature_mechanisms/stanza/000077500000000000000000000000001477105560000232335ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_mechanisms/stanza/__init__.py000066400000000000000000000012171477105560000253450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms from slixmpp.features.feature_mechanisms.stanza.auth import Auth from slixmpp.features.feature_mechanisms.stanza.success import Success from slixmpp.features.feature_mechanisms.stanza.failure import Failure from slixmpp.features.feature_mechanisms.stanza.challenge import Challenge from slixmpp.features.feature_mechanisms.stanza.response import Response from slixmpp.features.feature_mechanisms.stanza.abort import Abort slixmpp/slixmpp/features/feature_mechanisms/stanza/abort.py000066400000000000000000000010011477105560000247040ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import StanzaBase from typing import ClassVar, Set class Abort(StanzaBase): """ """ name = 'abort' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces: ClassVar[Set[str]] = set() plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() slixmpp/slixmpp/features/feature_mechanisms/stanza/auth.py000066400000000000000000000023041477105560000245450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Auth(StanzaBase): """ """ name = 'auth' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'mechanism', 'value'} plugin_attrib = name #: Some SASL mechs require sending values as is, #: without converting base64. plain_mechs = {'X-MESSENGER-OAUTH2'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): if not self['mechanism'] in self.plain_mechs: return base64.b64decode(bytes(self.xml.text)) else: return self.xml.text def set_value(self, values): if not self['mechanism'] in self.plain_mechs: if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') elif values == b'': self.xml.text = '=' else: self.xml.text = bytes(values).decode('utf-8') def del_value(self): self.xml.text = '' slixmpp/slixmpp/features/feature_mechanisms/stanza/challenge.py000066400000000000000000000014731477105560000255340ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Challenge(StanzaBase): """ """ name = 'challenge' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp/slixmpp/features/feature_mechanisms/stanza/failure.py000066400000000000000000000044761477105560000252470ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import StanzaBase, ET class Failure(StanzaBase): """ """ name = 'failure' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'condition', 'text'} plugin_attrib = name sub_interfaces = {'text'} conditions = {'aborted', 'account-disabled', 'credentials-expired', 'encryption-required', 'incorrect-encoding', 'invalid-authzid', 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak', 'not-authorized', 'temporary-auth-failure'} def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup. Sets a default error type and condition, and changes the parent stanza's type to 'error'. Arguments: xml -- Use an existing XML object for the stanza's values. """ # StanzaBase overrides self.namespace self.namespace = Failure.namespace if StanzaBase.setup(self, xml): #If we had to generate XML then set default values. self['condition'] = 'not-authorized' self.xml.tag = self.tag_name() def get_condition(self): """Return the condition element's name.""" for child in self.xml: if "{%s}" % self.namespace in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: return cond return 'not-authorized' def set_condition(self, value): """ Set the tag name of the condition element. Arguments: value -- The tag name of the condition element. """ if value in self.conditions: del self['condition'] self.xml.append(ET.Element("{%s}%s" % (self.namespace, value))) return self def del_condition(self): """Remove the condition element.""" for child in self.xml: if "{%s}" % self.namespace in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: self.xml.remove(child) return self slixmpp/slixmpp/features/feature_mechanisms/stanza/mechanisms.py000066400000000000000000000022611477105560000257350ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class Mechanisms(ElementBase): """ """ name = 'mechanisms' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'mechanisms', 'required'} plugin_attrib = name is_extension = True def get_required(self): """ """ return True def get_mechanisms(self): """ """ results = [] mechs = self.xml.findall('{%s}mechanism' % self.namespace) if mechs: for mech in mechs: results.append(mech.text) return results def set_mechanisms(self, values): """ """ self.del_mechanisms() for val in values: mech = ET.Element('{%s}mechanism' % self.namespace) mech.text = val self.append(mech) def del_mechanisms(self): """ """ mechs = self.xml.findall('{%s}mechanism' % self.namespace) if mechs: for mech in mechs: self.xml.remove(mech) slixmpp/slixmpp/features/feature_mechanisms/stanza/response.py000066400000000000000000000014711477105560000254460ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Response(StanzaBase): """ """ name = 'response' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp/slixmpp/features/feature_mechanisms/stanza/success.py000066400000000000000000000014661477105560000252640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import base64 from slixmpp.util import bytes from slixmpp.xmlstream import StanzaBase class Success(StanzaBase): """ """ name = 'success' namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' interfaces = {'value'} plugin_attrib = name def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') else: self.xml.text = '=' def del_value(self): self.xml.text = '' slixmpp/slixmpp/features/feature_preapproval/000077500000000000000000000000001477105560000221375ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_preapproval/__init__.py000066400000000000000000000006071477105560000242530ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_preapproval.preapproval import FeaturePreApproval from slixmpp.features.feature_preapproval.stanza import PreApproval register_plugin(FeaturePreApproval) slixmpp/slixmpp/features/feature_preapproval/preapproval.py000066400000000000000000000022701477105560000250450ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.stanza import StreamFeatures from slixmpp.features.feature_preapproval import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin from typing import ClassVar, Set log = logging.getLogger(__name__) class FeaturePreApproval(BasePlugin): name = 'feature_preapproval' description = 'RFC 6121: Stream Feature: Subscription Pre-Approval' dependencies: ClassVar[Set[str]] = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('preapproval', self._handle_preapproval, restart=False, order=9001) register_stanza_plugin(StreamFeatures, stanza.PreApproval) def _handle_preapproval(self, features): """Save notice that the server support subscription pre-approvals. Arguments: features -- The stream features stanza. """ log.debug("Server supports subscription pre-approvals.") self.xmpp.features.add('preapproval') slixmpp/slixmpp/features/feature_preapproval/stanza.py000066400000000000000000000006271477105560000240160ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase from typing import ClassVar, Set class PreApproval(ElementBase): name = 'sub' namespace = 'urn:xmpp:features:pre-approval' interfaces: ClassVar[Set[str]] = set() plugin_attrib = 'preapproval' slixmpp/slixmpp/features/feature_rosterver/000077500000000000000000000000001477105560000216375ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_rosterver/__init__.py000066400000000000000000000005731477105560000237550ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_rosterver.rosterver import FeatureRosterVer from slixmpp.features.feature_rosterver.stanza import RosterVer register_plugin(FeatureRosterVer) slixmpp/slixmpp/features/feature_rosterver/rosterver.py000066400000000000000000000021571477105560000242510ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.stanza import StreamFeatures from slixmpp.features.feature_rosterver import stanza from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.base import BasePlugin from typing import ClassVar, Set log = logging.getLogger(__name__) class FeatureRosterVer(BasePlugin): name = 'feature_rosterver' description = 'RFC 6121: Stream Feature: Roster Versioning' dependences: ClassVar[Set[str]] = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('rosterver', self._handle_rosterver, restart=False, order=9000) register_stanza_plugin(StreamFeatures, stanza.RosterVer) def _handle_rosterver(self, features): """Enable using roster versioning. Arguments: features -- The stream features stanza. """ log.debug("Enabling roster versioning.") self.xmpp.features.add('rosterver') slixmpp/slixmpp/features/feature_rosterver/stanza.py000066400000000000000000000006201477105560000235070ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase from typing import Set, ClassVar class RosterVer(ElementBase): name = 'ver' namespace = 'urn:xmpp:features:rosterver' interfaces: ClassVar[Set[str]] = set() plugin_attrib = 'rosterver' slixmpp/slixmpp/features/feature_session/000077500000000000000000000000001477105560000212675ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_session/__init__.py000066400000000000000000000005571477105560000234070ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_session.session import FeatureSession from slixmpp.features.feature_session.stanza import Session register_plugin(FeatureSession) slixmpp/slixmpp/features/feature_session/session.py000066400000000000000000000031511477105560000233240ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from slixmpp.stanza import Iq, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.features.feature_session import stanza from typing import ClassVar, Set log = logging.getLogger(__name__) class FeatureSession(BasePlugin): name = 'feature_session' description = 'RFC 3920: Stream Feature: Start Session' dependencies: ClassVar[Set[str]] = set() stanza = stanza def plugin_init(self): self.xmpp.register_feature('session', self._handle_start_session, restart=False, order=10001) register_stanza_plugin(Iq, stanza.Session) register_stanza_plugin(StreamFeatures, stanza.Session) async def _handle_start_session(self, features): """ Handle the start of the session. Arguments: feature -- The stream features element. """ if features['session']['optional']: self.xmpp.sessionstarted = True self.xmpp.event('session_start') return iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('session') await iq.send(callback=self._on_start_session_response) def _on_start_session_response(self, response): self.xmpp.features.add('session') log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') slixmpp/slixmpp/features/feature_session/stanza.py000066400000000000000000000014651477105560000231470ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class Session(ElementBase): """ """ name = 'session' namespace = 'urn:ietf:params:xml:ns:xmpp-session' interfaces = {'optional'} plugin_attrib = 'session' def get_optional(self): return self.xml.find('{%s}optional' % self.namespace) is not None def set_optional(self, value): if value: optional = ET.Element('{%s}optional' % self.namespace) self.xml.append(optional) else: self.del_optional() def del_optional(self): optional = self.xml.find('{%s}optional' % self.namespace) self.xml.remove(optional) slixmpp/slixmpp/features/feature_starttls/000077500000000000000000000000001477105560000214645ustar00rootroot00000000000000slixmpp/slixmpp/features/feature_starttls/__init__.py000066400000000000000000000005561477105560000236030ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.features.feature_starttls.starttls import FeatureSTARTTLS from slixmpp.features.feature_starttls.stanza import * register_plugin(FeatureSTARTTLS) slixmpp/slixmpp/features/feature_starttls/stanza.py000066400000000000000000000025221477105560000233370ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import Set, ClassVar from slixmpp.xmlstream import StanzaBase, ElementBase from slixmpp.xmlstream.xmlstream import InvalidCABundle import logging log = logging.getLogger(__name__) class STARTTLS(StanzaBase): """ .. code-block:: xml """ name = 'starttls' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces = {'required'} plugin_attrib = name def get_required(self): return True class Proceed(StanzaBase): """ .. code-block:: xml """ name = 'proceed' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces: ClassVar[Set[str]] = set() def exception(self, e: Exception) -> None: log.exception('Error handling {%s}%s stanza', self.namespace, self.name) if isinstance(e, InvalidCABundle): raise e class Failure(StanzaBase): """ .. code-block:: xml """ name = 'failure' namespace = 'urn:ietf:params:xml:ns:xmpp-tls' interfaces: ClassVar[Set[str]] = set() slixmpp/slixmpp/features/feature_starttls/starttls.py000066400000000000000000000040611477105560000237170ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.stanza import StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.features.feature_starttls import stanza from typing import ClassVar, Set log = logging.getLogger(__name__) class FeatureSTARTTLS(BasePlugin): name = 'feature_starttls' description = 'RFC 6120: Stream Feature: STARTTLS' dependencies: ClassVar[Set[str]] = set() stanza = stanza def plugin_init(self): self.xmpp.register_handler( CoroutineCallback('STARTTLS Proceed', MatchXPath(stanza.Proceed.tag_name()), self._handle_starttls_proceed, instream=True)) self.xmpp.register_feature('starttls', self._handle_starttls, restart=True, order=self.config.get('order', 0)) self.xmpp.register_stanza(stanza.Proceed) self.xmpp.register_stanza(stanza.Failure) register_stanza_plugin(StreamFeatures, stanza.STARTTLS) def _handle_starttls(self, features): """ Handle notification that the server supports TLS. Arguments: features -- The stream:features element. """ if 'starttls' in self.xmpp.features: # We have already negotiated TLS, but the server is # offering it again, against spec. return False elif not self.xmpp.enable_starttls: return False else: self.xmpp.send(stanza.STARTTLS()) return True async def _handle_starttls_proceed(self, proceed): """Restart the XML stream when TLS is accepted.""" log.debug("Starting TLS") if await self.xmpp.start_tls(): self.xmpp.features.add('starttls') slixmpp/slixmpp/jid.py000066400000000000000000000313671477105560000154050ustar00rootroot00000000000000 # slixmpp.jid # ~~~~~~~~~~~~~~~~~~~~~~~ # This module allows for working with Jabber IDs (JIDs). # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details from __future__ import annotations import re import socket from functools import lru_cache from typing import ( Optional, Union, ) from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError HAVE_INET_PTON = hasattr(socket, 'inet_pton') #: The basic regex pattern that a JID must match in order to determine #: the local, domain, and resource parts. This regex does NOT do any #: validation, which requires application of nodeprep, resourceprep, etc. JID_PATTERN = re.compile( "^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$" ) #: The set of escape sequences for the characters not allowed by nodeprep. JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f', '\\3a', '\\3c', '\\3e', '\\40', '\\5c'} #: The reverse mapping of escape sequences to their original forms. JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ', '\\22': '"', '\\26': '&', '\\27': "'", '\\2f': '/', '\\3a': ':', '\\3c': '<', '\\3e': '>', '\\40': '@', '\\5c': '\\'} # TODO: Find the best cache size for a standard usage. @lru_cache(maxsize=1024) def _parse_jid(data: str): """ Parse string data into the node, domain, and resource components of a JID, if possible. :param string data: A string that is potentially a JID. :raises InvalidJID: :returns: tuple of the validated local, domain, and resource strings """ match = JID_PATTERN.match(data) if not match: raise InvalidJID('JID could not be parsed') (node, domain, resource) = match.groups() node = _validate_node(node) domain = _validate_domain(domain) resource = _validate_resource(resource) return node, domain, resource def _validate_node(node: Optional[str]): """Validate the local, or username, portion of a JID. :raises InvalidJID: :returns: The local portion of a JID, as validated by nodeprep. """ if node is None: return '' try: node = nodeprep(node) except StringprepError: raise InvalidJID('Nodeprep failed') if not node: raise InvalidJID('Localpart must not be 0 bytes') if len(node) > 1023: raise InvalidJID('Localpart must be less than 1024 bytes') return node def _validate_domain(domain: str): """Validate the domain portion of a JID. IP literal addresses are left as-is, if valid. Domain names are stripped of any trailing label separators (`.`), and are checked with the nameprep profile of stringprep. If the given domain is actually a punyencoded version of a domain name, it is converted back into its original Unicode form. Domains must also not start or end with a dash (`-`). :raises InvalidJID: :returns: The validated domain name """ ip_addr = False # First, check if this is an IPv4 address try: socket.inet_aton(domain) ip_addr = True except socket.error: pass # Check if this is an IPv6 address if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']': try: ip = domain[1:-1] socket.inet_pton(socket.AF_INET6, ip) ip_addr = True except (socket.error, ValueError): pass if not ip_addr: # This is a domain name, which must be checked further if domain and domain[-1] == '.': domain = domain[:-1] try: domain = idna(domain) except StringprepError: raise InvalidJID(f'idna validation failed: {domain}') if ':' in domain: raise InvalidJID(f'Domain containing a port: {domain}') for label in domain.split('.'): if not label: raise InvalidJID(f'Domain containing too many dots: {domain}') if '-' in (label[0], label[-1]): raise InvalidJID(f'Domain starting or ending with -: {domain}') if not domain: raise InvalidJID('Domain must not be 0 bytes') if len(domain) > 1023: raise InvalidJID('Domain must be less than 1024 bytes') return domain def _validate_resource(resource: Optional[str]): """Validate the resource portion of a JID. :raises InvalidJID: :returns: The local portion of a JID, as validated by resourceprep. """ if resource is None: return '' try: resource = resourceprep(resource) except StringprepError: raise InvalidJID('Resourceprep failed') if not resource: raise InvalidJID('Resource must not be 0 bytes') if len(resource) > 1023: raise InvalidJID('Resource must be less than 1024 bytes') return resource def _unescape_node(node: str): """Unescape a local portion of a JID. .. note:: The unescaped local portion is meant ONLY for presentation, and should not be used for other purposes. """ unescaped = [] seq = '' for i, char in enumerate(node): if char == '\\': seq = node[i:i+3] if seq not in JID_ESCAPE_SEQUENCES: seq = '' if seq: if len(seq) == 3: unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char)) # Pop character off the escape sequence, and ignore it seq = seq[1:] else: unescaped.append(char) return ''.join(unescaped) def _format_jid( local: Optional[str] = None, domain: Optional[str] = None, resource: Optional[str] = None, ): """Format the given JID components into a full or bare JID. :param string local: Optional. The local portion of the JID. :param string domain: Required. The domain name portion of the JID. :param strin resource: Optional. The resource portion of the JID. :return: A full or bare JID string. """ if domain is None: return '' if local is not None: result = local + '@' + domain else: result = domain if resource is not None: result += '/' + resource return result class InvalidJID(ValueError): """ Raised when attempting to create a JID that does not pass validation. It can also be raised if modifying an existing JID in such a way as to make it invalid, such trying to remove the domain from an existing full JID while the local and resource portions still exist. """ # pylint: disable=R0903 class UnescapedJID: """ .. versionadded:: 1.1.10 """ __slots__ = ('_node', '_domain', '_resource') def __init__( self, node: Optional[str], domain: Optional[str], resource: Optional[str], ): self._node = node self._domain = domain self._resource = resource def __getattribute__(self, name: str): """Retrieve the given JID component. :param name: one of: user, server, domain, resource, full, or bare. """ if name == 'resource': return self._resource or '' if name in ('user', 'username', 'local', 'node'): return self._node or '' if name in ('server', 'domain', 'host'): return self._domain or '' if name in ('full', 'jid'): return _format_jid(self._node, self._domain, self._resource) if name == 'bare': return _format_jid(self._node, self._domain) return object.__getattribute__(self, name) def __str__(self): """Use the full JID as the string value.""" return _format_jid(self._node, self._domain, self._resource) def __repr__(self): """Use the full JID as the representation.""" return _format_jid(self._node, self._domain, self._resource) class JID: """ A representation of a Jabber ID, or JID. Each JID may have three components: a user, a domain, and an optional resource. For example: user@domain/resource When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. **JID Properties:** :full: The string value of the full JID. :jid: Alias for ``full``. :bare: The string value of the bare JID. :node: The node portion of the JID. :user: Alias for ``node``. :local: Alias for ``node``. :username: Alias for ``node``. :domain: The domain name portion of the JID. :server: Alias for ``domain``. :host: Alias for ``domain``. :resource: The resource portion of the JID. :param string jid: A string of the form ``'[user@]domain[/resource]'``. :param bool bare: If present, discard the provided resource. :raises InvalidJID: """ __slots__ = ('_node', '_domain', '_resource', '_bare', '_full') def __init__(self, jid: Optional[Union[str, 'JID']] = None, bare: bool = False): if not jid: self._node = '' self._domain = '' self._resource = '' self._bare = '' self._full = '' return elif not isinstance(jid, JID): node, domain, resource = _parse_jid(jid) self._node = node self._domain = domain self._resource = resource if not bare else '' else: self._node = jid._node self._domain = jid._domain self._resource = jid._resource if not bare else '' self._update_bare_full() def unescape(self): """Return an unescaped JID object. Using an unescaped JID is preferred for displaying JIDs to humans, and they should NOT be used for any other purposes than for presentation. :return: :class:`UnescapedJID` .. versionadded:: 1.1.10 """ return UnescapedJID(_unescape_node(self._node), self._domain, self._resource) def _update_bare_full(self): """Format the given JID into a bare and a full JID. """ self._bare = (self._node + '@' + self._domain if self._node else self._domain) self._full = (self._bare + '/' + self._resource if self._resource else self._bare) @property def bare(self) -> str: return self._bare @bare.setter def bare(self, value: str): node, domain, resource = _parse_jid(value) assert not resource self._node = node self._domain = domain self._update_bare_full() @property def node(self) -> str: return self._node @node.setter def node(self, value: Optional[str]): self._node = _validate_node(value) self._update_bare_full() @property def domain(self) -> str: return self._domain @domain.setter def domain(self, value: str): self._domain = _validate_domain(value) self._update_bare_full() @property def resource(self) -> str: return self._resource @resource.setter def resource(self, value: Optional[str]): self._resource = _validate_resource(value) self._update_bare_full() @property def full(self) -> str: return self._full @full.setter def full(self, value: str): self._node, self._domain, self._resource = _parse_jid(value) self._update_bare_full() user = node local = node username = node server = domain host = domain jid = full def __str__(self): """Use the full JID as the string value.""" return self._full def __repr__(self): """Use the full JID as the representation.""" return self._full # pylint: disable=W0212 def __eq__(self, other): """Two JIDs are equal if they have the same full JID value.""" if isinstance(other, UnescapedJID): return False if not isinstance(other, JID): try: other = JID(other) except InvalidJID: return NotImplemented return (self._node == other._node and self._domain == other._domain and self._resource == other._resource) def __ne__(self, other): """Two JIDs are considered unequal if they are not equal.""" return not self == other def __hash__(self): """Hash a JID based on the string version of its full JID.""" return hash(self._full) slixmpp/slixmpp/jid.rs000066400000000000000000000234241477105560000153740ustar00rootroot00000000000000use pyo3::exceptions::{PyNotImplementedError, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyString; pyo3::create_exception!(slixmpp.jid, InvalidJID, PyValueError, "Raised when attempting to create a JID that does not pass validation.\n\nIt can also be raised if modifying an existing JID in such a way as\nto make it invalid, such trying to remove the domain from an existing\nfull JID while the local and resource portions still exist."); fn to_exc(err: jid::Error) -> PyErr { InvalidJID::new_err(err.to_string()) } /// A representation of a Jabber ID, or JID. /// /// Each JID may have three components: a user, a domain, and an optional resource. For example: /// user@domain/resource /// /// When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. /// /// Raises InvalidJID if the parser rejects it. #[pyclass(name = "JID", module = "slixmpp.jid")] struct PyJid { jid: Option, } #[pymethods] impl PyJid { #[new] #[pyo3(signature = (jid=None, bare=false))] fn new(jid: Option<&Bound<'_, PyAny>>, bare: bool) -> PyResult { if let Some(jid) = jid { if let Ok(py_jid) = jid.extract::>() { if bare { if let Some(jid) = &(*py_jid).jid { Ok(PyJid { jid: Some(jid.to_bare().into()), }) } else { Ok(PyJid { jid: None }) } } else { Ok(PyJid { jid: (*py_jid).jid.clone(), }) } } else { let jid: &str = jid.extract()?; if jid.is_empty() { Ok(PyJid { jid: None }) } else { let mut jid = jid::Jid::new(jid).map_err(to_exc)?; if bare { jid = jid.into_bare().into() } Ok(PyJid { jid: Some(jid) }) } } } else { Ok(PyJid { jid: None }) } } /* // TODO: implement or remove from the API fn unescape() { } */ #[getter] fn get_bare(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid.to_bare().to_string(), } } #[setter] fn set_bare(&mut self, bare: &str) -> PyResult<()> { let bare = jid::BareJid::new(bare).map_err(to_exc)?; self.jid = Some(match self.jid.as_ref().map(jid::Jid::try_as_full) { Some(Ok(full)) => bare.with_resource(full.resource()).into(), Some(Err(_)) | None => bare.into(), }); Ok(()) } #[getter] fn get_full(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid.to_string(), } } #[setter] fn set_full(&mut self, full: &str) -> PyResult<()> { // JID.full = 'domain' is acceptable in slixmpp. self.jid = Some(jid::Jid::new(full).map_err(to_exc)?); Ok(()) } #[getter] fn get_node(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid .node() .map(ToString::to_string) .unwrap_or_else(String::new), } } #[setter] fn set_node(&mut self, node: Option<&str>) -> PyResult<()> { if let Some(node) = node { let node = jid::NodePart::new(node).map_err(to_exc)?; self.jid = Some(match self.jid.as_ref().map(jid::Jid::try_as_full) { Some(Ok(full)) => jid::FullJid::from_parts( Some(&node), full.domain(), full.resource(), ).into(), Some(Err(bare)) => { jid::BareJid::from_parts(Some(&node), bare.domain()).into() } None => Err(InvalidJID::new_err("JID.node must apply to a proper JID"))?, }); } else { if let Some(jid) = self.jid.take() { if jid.node().is_some() { self.jid = Some(jid::Jid::from_parts(None, jid.domain(), jid.resource())); } } } Ok(()) } #[getter] fn get_domain(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid.domain().to_string(), } } #[setter] fn set_domain(&mut self, domain: &str) -> PyResult<()> { let domain = jid::DomainPart::new(domain).map_err(to_exc)?; self.jid = Some(match self.jid.as_ref().map(jid::Jid::try_as_full) { Some(Ok(full)) => jid::FullJid::from_parts( full.node(), &domain, full.resource(), ).into(), Some(Err(bare)) => { jid::BareJid::from_parts(bare.node(), &domain).into() } None => jid::BareJid::from_parts(None, &domain).into(), }); Ok(()) } #[getter] fn get_resource(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid .resource() .map(ToString::to_string) .unwrap_or_else(String::new), } } #[setter] fn set_resource(&mut self, resource: Option<&str>) -> PyResult<()> { if let Some(resource) = resource { let resource = jid::ResourcePart::new(resource).map_err(to_exc)?; self.jid = Some(match self.jid.as_ref().map(jid::Jid::try_as_full) { Some(Ok(full)) => jid::FullJid::from_parts( full.node(), full.domain(), &resource, ).into(), Some(Err(bare)) => { bare.with_resource(&resource).into() } None => Err(InvalidJID::new_err( "JID.resource must apply to a proper JID", ))?, }); } else { if let Some(jid) = self.jid.take() { self.jid = Some(jid.into_bare().into()); } } Ok(()) } /// Use the full JID as the string value. fn __str__(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid.to_string(), } } /// Use the full JID as the representation. fn __repr__(&self) -> String { match &self.jid { None => String::new(), Some(jid) => jid.to_string(), } } /// Two JIDs are equal if they have the same full JID value. fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: pyo3::basic::CompareOp) -> PyResult { let other = if let Ok(other) = other.extract::>() { other } else if other.is_none() { Bound::new(other.py(), PyJid::new(None, false)?)?.borrow() } else { match PyJid::new(Some(other), false) { Ok(res) => Bound::new(other.py(), res)?.borrow(), Err(_) => return Ok(false), } }; match (&self.jid, &other.jid) { (None, None) => Ok(true), (Some(jid), Some(other)) => match op { pyo3::basic::CompareOp::Eq => Ok(jid == other), pyo3::basic::CompareOp::Ne => Ok(jid != other), _ => Err(PyNotImplementedError::new_err( "Only == and != are implemented", )), }, _ => Ok(false), } } /// Hash a JID based on the string version of its full JID. fn __hash__(&self, py: Python) -> PyResult { if let Some(jid) = &self.jid { // Use the same algorithm as the Python JID. PyString::new(py, jid.as_str()).hash() } else { Ok(0) } } fn __getstate__(&self, py: Python) -> PyResult> { match &self.jid { Some(jid) => Ok(Some(PyString::new(py, jid.as_str()).into())), None => Ok(None) } } fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> { if state.is_none(py) { self.jid = None; } else { let string = state.extract(py)?; self.jid = Some(jid::Jid::new(string).unwrap()); } Ok(()) } // Aliases #[getter] fn get_user(&self) -> String { self.get_node() } #[setter] fn set_user(&mut self, user: Option<&str>) -> PyResult<()> { self.set_node(user) } #[getter] fn get_local(&self) -> String { self.get_node() } #[setter] fn set_local(&mut self, local: Option<&str>) -> PyResult<()> { self.set_node(local) } #[getter] fn get_username(&self) -> String { self.get_node() } #[setter] fn set_username(&mut self, username: Option<&str>) -> PyResult<()> { self.set_node(username) } #[getter] fn get_server(&self) -> String { self.get_domain() } #[setter] fn set_server(&mut self, server: &str) -> PyResult<()> { self.set_domain(server) } #[getter] fn get_host(&self) -> String { self.get_domain() } #[setter] fn set_host(&mut self, host: &str) -> PyResult<()> { self.set_domain(host) } #[getter] fn get_jid(&self) -> String { self.get_full() } #[setter] fn set_jid(&mut self, jid: &str) -> PyResult<()> { self.set_full(jid) } } #[pymodule] #[pyo3(name = "jid")] fn py_jid(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add("InvalidJID", py.get_type::())?; Ok(()) } slixmpp/slixmpp/plugins/000077500000000000000000000000001477105560000157345ustar00rootroot00000000000000slixmpp/slixmpp/plugins/__init__.py000066400000000000000000000125411477105560000200500ustar00rootroot00000000000000# 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 slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin from slixmpp.plugins.base import register_plugin, load_plugin PLUGINS = [ # XEPS 'xep_0004', # Data Forms 'xep_0009', # Jabber-RPC 'xep_0012', # Last Activity 'xep_0013', # Flexible Offline Message Retrieval # 'xep_0016', # Privacy Lists. Don’t automatically load 'xep_0020', # Feature Negotiation 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery 'xep_0033', # Extended Stanza Addresses 'xep_0045', # Multi-User Chat (Client) 'xep_0047', # In-Band Bytestreams # 'xep_0048', # Legacy Bookmarks. Don’t automatically load 'xep_0049', # Private XML Storage 'xep_0050', # Ad-hoc Commands 'xep_0054', # vcard-temp 'xep_0055', # Jabber Search 'xep_0059', # Result Set Management 'xep_0060', # Pubsub (Client) 'xep_0065', # SOCKS5 Bytestreams 'xep_0066', # Out of Band Data 'xep_0070', # Verifying HTTP Requests via XMPP 'xep_0071', # XHTML-IM 'xep_0077', # In-Band Registration # 'xep_0078', # Non-SASL auth. Don’t automatically load 'xep_0079', # Advanced Message Processing 'xep_0080', # User Location 'xep_0082', # XMPP Date and Time Profiles 'xep_0084', # User Avatar 'xep_0085', # Chat State Notifications 'xep_0086', # Legacy Error Codes # 'xep_0091', # Legacy Delayed Delivery. Don’t automatically load 'xep_0092', # Software Version # 'xep_0095', # Legacy Stream Initiation. Don’t automatically load # 'xep_0096', # Legacy SI File Transfer. Don’t automatically load 'xep_0100', # Gateway interaction 'xep_0106', # JID Escaping 'xep_0107', # User Mood 'xep_0108', # User Activity 'xep_0115', # Entity Capabilities 'xep_0118', # User Tune 'xep_0122', # Data Forms Validation 'xep_0128', # Extended Service Discovery 'xep_0131', # Standard Headers and Internet Metadata 'xep_0133', # Service Administration 'xep_0152', # Reachability Addresses 'xep_0153', # vCard-Based Avatars 'xep_0163', # Personal Eventing Protocol 'xep_0172', # User Nickname 'xep_0184', # Message Receipts 'xep_0186', # Invisible Command 'xep_0191', # Blocking Command 'xep_0196', # User Gaming 'xep_0198', # Stream Management 'xep_0199', # Ping 'xep_0202', # Entity Time 'xep_0203', # Delayed Delivery 'xep_0221', # Data Forms Media Element 'xep_0222', # Persistent Storage of Public Data via Pubsub 'xep_0223', # Persistent Storage of Private Data via Pubsub 'xep_0224', # Attention 'xep_0231', # Bits of Binary 'xep_0235', # OAuth Over XMPP # 'xep_0242', # XMPP Client Compliance 2009. Don’t automatically load 'xep_0249', # Direct MUC Invitations 'xep_0256', # Last Activity in Presence 'xep_0257', # Client Certificate Management for SASL EXTERNAL 'xep_0258', # Security Labels in XMPP 'xep_0264', # Jingle Content Thumbnails # 'xep_0270', # XMPP Compliance Suites 2010. Don’t automatically load 'xep_0279', # Server IP Check 'xep_0280', # Message Carbons 'xep_0292', # vCard4 Over XMPP 'xep_0297', # Stanza Forwarding 'xep_0300', # Use of Cryptographic Hash Functions in XMPP # 'xep_0302', # XMPP Compliance Suites 2012. Don’t automatically load 'xep_0308', # Last Message Correction 'xep_0313', # Message Archive Management 'xep_0317', # Hats 'xep_0319', # Last User Interaction in Presence # 'xep_0323', # IoT Systems Sensor Data. Don’t automatically load # 'xep_0325', # IoT Systems Control. Don’t automatically load 'xep_0332', # HTTP Over XMPP Transport 'xep_0333', # Chat Markers 'xep_0334', # Message Processing Hints 'xep_0335', # JSON Containers 'xep_0352', # Client State Indication 'xep_0353', # Jingle Message Initiation 'xep_0356', # Privileged entity 'xep_0359', # Unique and Stable Stanza IDs 'xep_0363', # HTTP File Upload 'xep_0369', # MIX-CORE 'xep_0377', # Spam reporting 'xep_0380', # Explicit Message Encryption 'xep_0382', # Spoiler Messages 'xep_0385', # Stateless Inline Media Sharing (SIMS) 'xep_0394', # Message Markup 'xep_0402', # PEP Native Bookmarks 'xep_0403', # MIX-Presence 'xep_0404', # MIX-Anon 'xep_0405', # MIX-PAM 'xep_0410', # MUC Self-ping 'xep_0421', # Anonymous unique occupant identifiers for MUCs 'xep_0422', # Message Fastening 'xep_0424', # Message Retraction 'xep_0425', # Moderated Message Retraction 'xep_0428', # Message Fallback 'xep_0437', # Room Activity Indicators 'xep_0439', # Quick Response 'xep_0441', # Message Archive Management Preferences 'xep_0444', # Message Reactions 'xep_0446', # File metadata element 'xep_0447', # Stateless file sharing 'xep_0461', # Message Replies 'xep_0469', # Bookmarks Pinning 'xep_0482', # Call Invites 'xep_0490', # Message Displayed Synchronization 'xep_0492', # Chat Notification Settings # Meant to be imported by plugins ] __all__ = PLUGINS + [ 'PluginManager', 'PluginNotFound', 'BasePlugin', 'register_plugin', 'load_plugin', ] slixmpp/slixmpp/plugins/base.py000066400000000000000000000302501477105560000172200ustar00rootroot00000000000000 # slixmpp.plugins.base # ~~~~~~~~~~~~~~~~~~~~~~ # This module provides XMPP functionality that # is specific to client connections. # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2012 Nathanael C. Fritz # :license: MIT, see LICENSE for more details from __future__ import annotations import sys import copy import logging import threading from typing import Any, Dict, Set, ClassVar, Union, Optional, TYPE_CHECKING if TYPE_CHECKING: from slixmpp.clientxmpp import ClientXMPP, BaseXMPP from slixmpp.componentxmpp import ComponentXMPP log = logging.getLogger(__name__) #: Associate short string names of plugins with implementations. The #: plugin names are based on the spec used by the plugin, such as #: `'xep_0030'` for a plugin that implements XEP-0030. PLUGIN_REGISTRY = {} #: In order to do cascading plugin disabling, reverse dependencies #: must be tracked. PLUGIN_DEPENDENTS = {} #: Only allow one thread to manipulate the plugin registry at a time. REGISTRY_LOCK = threading.RLock() class PluginNotFound(Exception): """Raised if an unknown plugin is accessed.""" def register_plugin(impl, name=None): """Add a new plugin implementation to the registry. :param class impl: The plugin class. The implementation class must provide a :attr:`~BasePlugin.name` value that will be used as a short name for enabling and disabling the plugin. The name should be based on the specification used by the plugin. For example, a plugin implementing XEP-0030 would be named `'xep_0030'`. """ if name is None: name = impl.name with REGISTRY_LOCK: PLUGIN_REGISTRY[name] = impl if name not in PLUGIN_DEPENDENTS: PLUGIN_DEPENDENTS[name] = set() for dep in impl.dependencies: if dep not in PLUGIN_DEPENDENTS: PLUGIN_DEPENDENTS[dep] = set() PLUGIN_DEPENDENTS[dep].add(name) def load_plugin(name, module=None): """Find and import a plugin module so that it can be registered. This function is called to import plugins that have selected for enabling, but no matching registered plugin has been found. :param str name: The name of the plugin. It is expected that plugins are in packages matching their name, even though the plugin class name does not have to match. :param str module: The name of the base module to search for the plugin. """ try: if not module: try: module = 'slixmpp.plugins.%s' % name __import__(module) mod = sys.modules[module] except ImportError: module = 'slixmpp.features.%s' % name __import__(module) mod = sys.modules[module] elif isinstance(module, str): __import__(module) mod = sys.modules[module] else: mod = module # Add older style plugins to the registry. if hasattr(mod, name): plugin = getattr(mod, name) if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'): plugin.name = name # Mark the plugin as an older style plugin so # we can work around dependency issues. plugin.old_style = True register_plugin(plugin, name) except ImportError: log.exception("Unable to load plugin: %s", name) class PluginManager(object): def __init__(self, xmpp: 'BaseXMPP', config: Optional[Dict] = None): #: We will track all enabled plugins in a set so that we #: can enable plugins in batches and pull in dependencies #: without problems. self._enabled: Set[str] = set() #: Maintain references to active plugins. self._plugins: Dict[str, 'BasePlugin'] = {} self._plugin_lock = threading.RLock() #: Globally set default plugin configuration. This will #: be used for plugins that are auto-enabled through #: dependency loading. self.config = config if config else {} self.xmpp = xmpp def register(self, plugin, enable=True): """Register a new plugin, and optionally enable it. :param class plugin: The implementation class of the plugin to register. :param bool enable: If ``True``, immediately enable the plugin after registration. """ register_plugin(plugin) if enable: self.enable(plugin.name) def enable(self, name, config=None, enabled=None): """Enable a plugin, including any dependencies. :param string name: The short name of the plugin. :param dict config: Optional settings dictionary for configuring plugin behaviour. """ if enabled is None: enabled = set() with self._plugin_lock: if name not in self._enabled: enabled.add(name) self._enabled.add(name) if not self.registered(name): load_plugin(name) plugin_class = PLUGIN_REGISTRY.get(name, None) if not plugin_class: raise PluginNotFound(name) if config is None: config = self.config.get(name, None) plugin = plugin_class(self.xmpp, config) self._plugins[name] = plugin for dep in plugin.dependencies: self.enable(dep, enabled=enabled) plugin._init() for name in enabled: if hasattr(self._plugins[name], 'old_style'): # Older style plugins require post_init() # to run just before stream processing begins, # so we don't call it here. pass else: self._plugins[name].post_init() def enable_all(self, names=None, config=None): """Enable all registered plugins. :param list names: A list of plugin names to enable. If none are provided, all registered plugins will be enabled. :param dict config: A dictionary mapping plugin names to configuration dictionaries, as used by :meth:`~PluginManager.enable`. """ names = names if names else PLUGIN_REGISTRY.keys() if config is None: config = {} for name in names: self.enable(name, config.get(name, {})) def enabled(self, name): """Check if a plugin has been enabled. :param string name: The name of the plugin to check. :return: boolean """ return name in self._enabled def registered(self, name): """Check if a plugin has been registered. :param string name: The name of the plugin to check. :return: boolean """ return name in PLUGIN_REGISTRY def disable(self, name, _disabled=None): """Disable a plugin, including any dependent upon it. :param string name: The name of the plugin to disable. :param set _disabled: Private set used to track the disabled status of plugins during the cascading process. """ if _disabled is None: _disabled = set() with self._plugin_lock: if name not in _disabled and name in self._enabled: _disabled.add(name) plugin = self._plugins.get(name, None) if plugin is None: raise PluginNotFound(name) for dep in PLUGIN_DEPENDENTS[name]: self.disable(dep, _disabled) plugin._end() if name in self._enabled: self._enabled.remove(name) del self._plugins[name] def __keys__(self): """Return the set of enabled plugins.""" return self._plugins.keys() def __getitem__(self, name): """ Allow plugins to be accessed through the manager as if it were a dictionary. """ plugin = self._plugins.get(name, None) if plugin is None: raise PluginNotFound(name) return plugin def get(self, name: str, default: Optional['BasePlugin']) -> Optional['BasePlugin']: return self._plugins.get(name, default) def __iter__(self): """Return an iterator over the set of enabled plugins.""" return self._plugins.__iter__() def __len__(self): """Return the number of enabled plugins.""" return len(self._plugins) class BasePlugin(object): #: A short name for the plugin based on the implemented specification. #: For example, a plugin for XEP-0030 would use `'xep_0030'`. name: str = '' #: A longer name for the plugin, describing its purpose. For example, #: a plugin for XEP-0030 would use `'Service Discovery'` as its #: description value. description: str = '' #: Some plugins may depend on others in order to function properly. #: Any plugin names included in :attr:`~BasePlugin.dependencies` will #: be initialized as needed if this plugin is enabled. dependencies: ClassVar[Set[str]] = set() #: The basic, standard configuration for the plugin, which may #: be overridden when initializing the plugin. The configuration #: fields included here may be accessed directly as attributes of #: the plugin. For example, including the configuration field 'foo' #: would mean accessing `plugin.foo` returns the current value of #: `plugin.config['foo']`. default_config: ClassVar[Dict[str, Any]] = {} def __init__(self, xmpp: Union[ClientXMPP,ComponentXMPP], config=None): self.xmpp = xmpp if self.xmpp: self.api = self.xmpp.api.wrap(self.name) #: A plugin's behaviour may be configurable, in which case those #: configuration settings will be provided as a dictionary. self.config = copy.copy(self.default_config) if config: self.config.update(config) def __getattr__(self, key): """Provide direct access to configuration fields. If the standard configuration includes the option `'foo'`, then accessing `self.foo` should be the same as `self.config['foo']`. """ if key in self.default_config: return self.config.get(key, None) else: return object.__getattribute__(self, key) def __setattr__(self, key, value): """Provide direct assignment to configuration fields. If the standard configuration includes the option `'foo'`, then assigning to `self.foo` should be the same as assigning to `self.config['foo']`. """ if key in self.default_config: self.config[key] = value else: super().__setattr__(key, value) def _init(self): """Initialize plugin state, such as registering event handlers. Also sets up required event handlers. """ if self.xmpp is not None: self.xmpp.add_event_handler('session_bind', self.session_bind) if self.xmpp.session_bind_event.is_set(): self.session_bind(self.xmpp.boundjid.full) self.plugin_init() log.debug('Loaded Plugin: %s', self.description) def _end(self): """Cleanup plugin state, and prepare for plugin removal. Also removes required event handlers. """ if self.xmpp is not None: self.xmpp.del_event_handler('session_bind', self.session_bind) self.plugin_end() log.debug('Disabled Plugin: %s' % self.description) def plugin_init(self): """Initialize plugin state, such as registering event handlers.""" pass def plugin_end(self): """Cleanup plugin state, and prepare for plugin removal.""" pass def session_bind(self, jid): """Initialize plugin state based on the bound JID.""" pass def post_init(self): """Initialize any cross-plugin state. Only needed if the plugin has circular dependencies. """ pass slixmpp/slixmpp/plugins/xep_0004/000077500000000000000000000000001477105560000171735ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0004/__init__.py000066400000000000000000000006461477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0004.stanza import Form from slixmpp.plugins.xep_0004.stanza import FormField, FieldOption from slixmpp.plugins.xep_0004.dataforms import XEP_0004 register_plugin(XEP_0004) slixmpp/slixmpp/plugins/xep_0004/dataforms.py000066400000000000000000000031361477105560000215300ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import stanza from slixmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption class XEP_0004(BasePlugin): """ XEP-0004: Data Forms """ name = 'xep_0004' description = 'XEP-0004: Data Forms' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Data Form', StanzaPath('message/form'), self.handle_form)) register_stanza_plugin(FormField, FieldOption, iterable=True) register_stanza_plugin(Form, FormField, iterable=True) register_stanza_plugin(Message, Form) def plugin_end(self): self.xmpp.remove_handler('Data Form') self.xmpp['xep_0030'].del_feature(feature='jabber:x:data') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('jabber:x:data') def make_form(self, ftype='form', title='', instructions=''): f = Form() f['type'] = ftype f['title'] = title f['instructions'] = instructions return f def handle_form(self, message): self.xmpp.event("message_xform", message) def build_form(self, xml): return Form(xml=xml) slixmpp/slixmpp/plugins/xep_0004/stanza/000077500000000000000000000000001477105560000204735ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0004/stanza/__init__.py000066400000000000000000000004531477105560000226060ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.xep_0004.stanza.field import FormField, FieldOption from slixmpp.plugins.xep_0004.stanza.form import Form slixmpp/slixmpp/plugins/xep_0004/stanza/field.py000066400000000000000000000155601477105560000221370ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.xmlstream import ElementBase, ET class FormField(ElementBase): namespace = 'jabber:x:data' name = 'field' plugin_attrib = 'field' plugin_multi_attrib = 'fields' interfaces = {'answer', 'desc', 'required', 'value', 'label', 'type', 'var'} sub_interfaces = {'desc'} plugin_tag_map = {} plugin_attrib_map = {} field_types = {'boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single'} true_values = {True, '1', 'true'} option_types = {'list-multi', 'list-single'} multi_line_types = {'hidden', 'text-multi'} multi_value_types = {'hidden', 'jid-multi', 'list-multi', 'text-multi'} def setup(self, xml=None): if ElementBase.setup(self, xml): self._type = None else: self._type = self['type'] def set_type(self, value): self._set_attr('type', value) if value: self._type = value def add_option(self, label='', value=''): if self._type is None or self._type in self.option_types: opt = FieldOption() opt['label'] = label opt['value'] = value self.append(opt) else: raise ValueError("Cannot add options to " + \ "a %s field." % self['type']) def del_options(self): optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: self.xml.remove(optXML) def del_required(self): reqXML = self.xml.find('{%s}required' % self.namespace) if reqXML is not None: self.xml.remove(reqXML) def del_value(self): valsXML = self.xml.findall('{%s}value' % self.namespace) for valXML in valsXML: self.xml.remove(valXML) def get_answer(self): return self['value'] def get_options(self): options = [] optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: opt = FieldOption(xml=optXML) options.append({'label': opt['label'], 'value': opt['value']}) return options def get_required(self): reqXML = self.xml.find('{%s}required' % self.namespace) return reqXML is not None def get_value(self, convert=True, convert_list=False): """ Gets the value for this field :param convert: Convert truthy values to boolean :param convert_list: Convert text-multi fields to a string with \n as separator for values """ valsXML = self.xml.findall('{%s}value' % self.namespace) if len(valsXML) == 0: return None elif self._type == 'boolean': if convert: return valsXML[0].text in self.true_values return valsXML[0].text elif self._type in self.multi_value_types or len(valsXML) > 1: values = [] for valXML in valsXML: if valXML.text is None: valXML.text = '' values.append(valXML.text) if self._type == 'text-multi' and convert_list: values = "\n".join(values) return values else: if valsXML[0].text is None: return '' return valsXML[0].text def set_answer(self, answer): self['value'] = answer def set_false(self): self['value'] = False def set_options(self, options): for value in options: if isinstance(value, dict): self.add_option(**value) else: self.add_option(value=value) def set_required(self, required): exists = self['required'] if not exists and required: self.xml.append(ET.Element('{%s}required' % self.namespace)) elif exists and not required: del self['required'] def set_true(self): self['value'] = True def set_value(self, value): del self['value'] valXMLName = '{%s}value' % self.namespace if not self._type: if isinstance(value, bool): log.debug("Passed a 'boolean' as value of an untyped field, assuming it is a 'boolean'") self._type = "boolean" elif isinstance(value, str): log.debug("Passed a 'str' as value of an untyped field, assuming it is a 'text-single'") self._type = "text-single" elif isinstance(value, (list, tuple)): log.debug("Passed a %s as value of an untyped field, assuming it is a 'text-multi'") self._type = "text-multi" if self._type == 'boolean': if value in self.true_values: valXML = ET.Element(valXMLName) valXML.text = '1' self.xml.append(valXML) else: valXML = ET.Element(valXMLName) valXML.text = '0' self.xml.append(valXML) elif self._type in self.multi_value_types or self._type in ('', None): if isinstance(value, bool): value = [value] if not isinstance(value, list): value = value.replace('\r', '') value = value.split('\n') for val in value: if self._type in ('', None) and val in self.true_values: val = '1' valXML = ET.Element(valXMLName) valXML.text = val self.xml.append(valXML) else: if isinstance(value, list): raise ValueError("Cannot add multiple values " + \ "to a %s field." % self._type) valXML = ET.Element(valXMLName) valXML.text = value self.xml.append(valXML) class FieldOption(ElementBase): namespace = 'jabber:x:data' name = 'option' plugin_attrib = 'option' interfaces = {'label', 'value'} sub_interfaces = {'value'} plugin_multi_attrib = 'options' FormField.addOption = FormField.add_option FormField.delOptions = FormField.del_options FormField.delRequired = FormField.del_required FormField.delValue = FormField.del_value FormField.getAnswer = FormField.get_answer FormField.getOptions = FormField.get_options FormField.getRequired = FormField.get_required FormField.getValue = FormField.get_value FormField.setAnswer = FormField.set_answer FormField.setFalse = FormField.set_false FormField.setOptions = FormField.set_options FormField.setRequired = FormField.set_required FormField.setTrue = FormField.set_true FormField.setValue = FormField.set_value log = logging.getLogger(__name__) slixmpp/slixmpp/plugins/xep_0004/stanza/form.py000066400000000000000000000206611477105560000220150ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import copy import logging from slixmpp.thirdparty import OrderedSet from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins.xep_0004.stanza import FormField log = logging.getLogger(__name__) class Form(ElementBase): namespace = 'jabber:x:data' name = 'x' plugin_attrib = 'form' plugin_multi_attrib = 'forms' interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', 'values')) sub_interfaces = {'title'} form_types = {'cancel', 'form', 'result', 'submit'} def __init__(self, *args, **kwargs): title = None if 'title' in kwargs: title = kwargs['title'] del kwargs['title'] ElementBase.__init__(self, *args, **kwargs) if title is not None: self['title'] = title def setup(self, xml=None): if ElementBase.setup(self, xml): # If we had to generate xml self['type'] = 'form' @property def field(self): return self.get_fields() def set_type(self, ftype): self._set_attr('type', ftype) if ftype == 'submit': fields = self.get_fields() for var in fields: field = fields[var] if field['type'] != 'hidden': del field['type'] del field['label'] del field['desc'] del field['required'] del field['options'] elif ftype == 'cancel': del self['fields'] def add_field(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype field = FormField() field['var'] = var field['type'] = kwtype field['value'] = value if self['type'] in ('form', 'result'): field['label'] = label field['desc'] = desc field['required'] = required if options is not None: for option in options: field.add_option(**option) else: if field['type'] != 'hidden': del field['type'] self.append(field) return field def add_item(self, values): itemXML = ET.Element('{%s}item' % self.namespace) self.xml.append(itemXML) reported_vars = self.get_reported().keys() for var in reported_vars: field = FormField() field._type = self.get_reported()[var]['type'] field['var'] = var field['value'] = values.get(var, None) itemXML.append(field.xml) def add_reported(self, var, ftype=None, label='', desc='', **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) self.xml.append(reported) fieldXML = ET.Element('{%s}field' % FormField.namespace) reported.append(fieldXML) field = FormField(xml=fieldXML) field['var'] = var field['type'] = kwtype field['label'] = label field['desc'] = desc return field def cancel(self): self['type'] = 'cancel' def del_fields(self): fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: self.xml.remove(fieldXML) def del_instructions(self): instsXML = self.xml.findall('{%s}instructions') for instXML in instsXML: self.xml.remove(instXML) def del_items(self): itemsXML = self.xml.find('{%s}item' % self.namespace) for itemXML in itemsXML: self.xml.remove(itemXML) def del_reported(self): reportedXML = self.xml.find('{%s}reported' % self.namespace) if reportedXML is not None: self.xml.remove(reportedXML) def get_fields(self, use_dict=False): fields = {} for stanza in self['substanzas']: if isinstance(stanza, FormField): fields[stanza['var']] = stanza return fields def get_instructions(self): instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) def get_items(self): items = [] itemsXML = self.xml.findall('{%s}item' % self.namespace) for itemXML in itemsXML: item = {} fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) item[field['var']] = field['value'] items.append(item) return items def get_reported(self): fields = {} xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, FormField.namespace)) for field in xml: field = FormField(xml=field) fields[field['var']] = field return fields def get_values(self): values = {} fields = self.get_fields() for var in fields: values[var] = fields[var]['value'] return values def reply(self): if self['type'] == 'form': self['type'] = 'submit' elif self['type'] == 'submit': self['type'] = 'result' def set_fields(self, fields): del self['fields'] if not isinstance(fields, list): fields = fields.items() for var, field in fields: field['var'] = var self.add_field( var=field.get('var'), label=field.get('label'), desc=field.get('desc'), required=field.get('required'), value=field.get('value'), options=field.get('options'), type=field.get('type')) def set_instructions(self, instructions): del self['instructions'] if instructions in [None, '']: return if not isinstance(instructions, list): instructions = instructions.split('\n') for instruction in instructions: inst = ET.Element('{%s}instructions' % self.namespace) inst.text = instruction self.xml.append(inst) def set_items(self, items): for item in items: self.add_item(item) def set_reported(self, reported): """ This either needs a dictionary of dictionaries or a dictionary of form fields. :param reported: :return: """ for var in reported: field = reported[var] if isinstance(field, dict): self.add_reported(**field) else: reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) self.xml.append(reported) fieldXML = ET.Element('{%s}field' % FormField.namespace) reported.append(fieldXML) new_field = FormField(xml=fieldXML) new_field.values = field.values def set_values(self, values): fields = self.get_fields() for field in values: if field not in self.get_fields(): fields[field] = self.add_field(var=field) self.get_fields()[field]['value'] = values[field] def merge(self, other): new = copy.copy(self) if type(other) == dict: new['values'] = other return new nfields = new['fields'] ofields = other['fields'] nfields.update(ofields) new['fields'] = nfields return new Form.addField = Form.add_field Form.addReported = Form.add_reported Form.delFields = Form.del_fields Form.delInstructions = Form.del_instructions Form.delReported = Form.del_reported Form.getFields = Form.get_fields Form.getInstructions = Form.get_instructions Form.getReported = Form.get_reported Form.getValues = Form.get_values Form.setFields = Form.set_fields Form.setInstructions = Form.set_instructions Form.setReported = Form.set_reported Form.setValues = Form.set_values slixmpp/slixmpp/plugins/xep_0009/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0009/__init__.py000066400000000000000000000006601477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0009 import stanza from slixmpp.plugins.xep_0009.rpc import XEP_0009 from slixmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse register_plugin(XEP_0009) slixmpp/slixmpp/plugins/xep_0009/binding.py000066400000000000000000000131401477105560000211630ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ET import base64 import logging import time log = logging.getLogger(__name__) _namespace = 'jabber:iq:rpc' def fault2xml(fault): value = dict() value['faultCode'] = fault['code'] value['faultString'] = fault['string'] fault = ET.Element("fault", {'xmlns': _namespace}) fault.append(_py2xml((value))) return fault def xml2fault(params): vals = [] for value in params.findall('{%s}value' % _namespace): vals.append(_xml2py(value)) fault = dict() fault['code'] = vals[0]['faultCode'] fault['string'] = vals[0]['faultString'] return fault def py2xml(*args): params = ET.Element("{%s}params" % _namespace) for x in args: param = ET.Element("{%s}param" % _namespace) param.append(_py2xml(x)) params.append(param) #... return params def _py2xml(*args): for x in args: val = ET.Element("{%s}value" % _namespace) if x is None: nil = ET.Element("{%s}nil" % _namespace) val.append(nil) elif type(x) is int: i4 = ET.Element("{%s}i4" % _namespace) i4.text = str(x) val.append(i4) elif type(x) is bool: boolean = ET.Element("{%s}boolean" % _namespace) boolean.text = str(int(x)) val.append(boolean) elif type(x) is str: string = ET.Element("{%s}string" % _namespace) string.text = x val.append(string) elif type(x) is float: double = ET.Element("{%s}double" % _namespace) double.text = str(x) val.append(double) elif type(x) is rpcbase64: b64 = ET.Element("{%s}base64" % _namespace) b64.text = x.encoded() val.append(b64) elif type(x) is rpctime: iso = ET.Element("{%s}dateTime.iso8601" % _namespace) iso.text = str(x) val.append(iso) elif type(x) in (list, tuple): array = ET.Element("{%s}array" % _namespace) data = ET.Element("{%s}data" % _namespace) for y in x: data.append(_py2xml(y)) array.append(data) val.append(array) elif type(x) is dict: struct = ET.Element("{%s}struct" % _namespace) for y in x.keys(): member = ET.Element("{%s}member" % _namespace) name = ET.Element("{%s}name" % _namespace) name.text = y member.append(name) member.append(_py2xml(x[y])) struct.append(member) val.append(struct) return val def xml2py(params): namespace = 'jabber:iq:rpc' vals = [] for param in params.findall('{%s}param' % namespace): vals.append(_xml2py(param.find('{%s}value' % namespace))) return vals def _xml2py(value): namespace = 'jabber:iq:rpc' find_value = value.find if find_value('{%s}nil' % namespace) is not None: return None if find_value('{%s}i4' % namespace) is not None: return int(find_value('{%s}i4' % namespace).text) if find_value('{%s}int' % namespace) is not None: return int(find_value('{%s}int' % namespace).text) if find_value('{%s}boolean' % namespace) is not None: return bool(int(find_value('{%s}boolean' % namespace).text)) if find_value('{%s}string' % namespace) is not None: return find_value('{%s}string' % namespace).text if find_value('{%s}double' % namespace) is not None: return float(find_value('{%s}double' % namespace).text) if find_value('{%s}base64' % namespace) is not None: return rpcbase64(find_value('{%s}base64' % namespace).text.encode()) if find_value('{%s}Base64' % namespace) is not None: # Older versions of XEP-0009 used Base64 return rpcbase64(find_value('{%s}Base64' % namespace).text.encode()) if find_value('{%s}dateTime.iso8601' % namespace) is not None: return rpctime(find_value('{%s}dateTime.iso8601' % namespace).text) if find_value('{%s}struct' % namespace) is not None: struct = {} for member in find_value('{%s}struct' % namespace).findall('{%s}member' % namespace): struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace)) return struct if find_value('{%s}array' % namespace) is not None: array = [] for val in find_value('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace): array.append(_xml2py(val)) return array raise ValueError() class rpcbase64: def __init__(self, data): #base 64 encoded string self.data = data def decode(self): return base64.b64decode(self.data) def __str__(self): return self.decode().decode() def encoded(self): return self.data.decode() class rpctime: def __init__(self,data=None): #assume string data is in iso format YYYYMMDDTHH:MM:SS if type(data) is str: self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S") elif type(data) is time.struct_time: self.timestamp = data elif data is None: self.timestamp = time.gmtime() else: raise ValueError() def iso8601(self): #return a iso8601 string return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp) def __str__(self): return self.iso8601() slixmpp/slixmpp/plugins/xep_0009/remote.py000066400000000000000000000470011477105560000210470ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml from slixmpp import ClientXMPP from slixmpp.jid import JID from slixmpp.exceptions import IqError import abc import asyncio import inspect import logging import slixmpp import sys import traceback log = logging.getLogger(__name__) def _isstr(obj): return isinstance(obj, str) def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args) try: value = method(instance, *args, **kwargs) if value == NotImplemented: raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) return value except InvocationException: raise except Exception as e: traceback.print_exc() raise InvocationException("A problem occurred calling %s.%s!" % (instance.FQN(), method.__name__), e) _resolver._rpc = public _resolver._rpc_name = method.__name__ if name is None else name return _resolver def remote(function_argument, public = True): """ Decorator for methods which are remotely callable. This decorator works in conjunction with classes which extend ABC Endpoint. Example: @remote def remote_method(arg1, arg2) Arguments: function_argument -- a stand-in for either the actual method OR a new name (string) for the method. In that case the method is considered mapped: Example: @remote("new_name") def remote_method(arg1, arg2) public -- A flag which indicates if this method should be part of the known dictionary of remote methods. Defaults to True. Example: @remote(False) def remote_method(arg1, arg2) Note: renaming and revising (public vs. private) can be combined. Example: @remote("new_name", False) def remote_method(arg1, arg2) """ if hasattr(function_argument, '__call__'): return _intercept(function_argument, None, public) else: if not _isstr(function_argument): if not isinstance(function_argument, bool): raise Exception('Expected an RPC method name or visibility modifier!') else: def _wrap_revised(function): function = _intercept(function, None, function_argument) return function return _wrap_revised def _wrap_remapped(function): function = _intercept(function, function_argument, public) return function return _wrap_remapped class ACL: """ An Access Control List (ACL) is a list of rules, which are evaluated in order until a match is found. The policy of the matching rule is then applied. Rules are 3-tuples, consisting of a policy enumerated type, a JID expression and a RCP resource expression. Examples: [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'), (ACL.DENY, '*', '*') ] deny everyone everything, except named JID, which is allowed access to endpoint 'test' only. The use of wildcards is allowed in expressions, as follows: '*' everyone, or everything (= all endpoints and methods) 'test@xmpp.org/*' every JID regardless of JID resource '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc' 'frank@*' every 'frank', regardless of domain or JID res 'system.*' all methods of endpoint 'system' '*.reboot' all methods reboot regardless of endpoint """ ALLOW = True DENY = False @classmethod def check(cls, rules, jid, resource): if rules is None: return cls.DENY # No rules means no access! jid = str(jid) # Check the string representation of the JID. if not jid: return cls.DENY # Can't check an empty JID. for rule in rules: policy = cls._check(rule, jid, resource) if policy is not None: return policy return cls.DENY # By default if not rule matches, deny access. @classmethod def _check(cls, rule, jid, resource): if cls._match(jid, rule[1]) and cls._match(resource, rule[2]): return rule[0] else: return None @classmethod def _next_token(cls, expression, index): new_index = expression.find('*', index) if new_index == 0: return '' else: if new_index == -1: return expression[index : ] else: return expression[index : new_index] @classmethod def _match(cls, value, expression): index = 0 position = 0 while index < len(expression): token = cls._next_token(expression, index) size = len(token) if size > 0: token_index = value.find(token, position) if token_index == -1: return False else: position = token_index + len(token) if size == 0: index += 1 else: index += size return True ANY_ALL = [ (ACL.ALLOW, '*', '*') ] class RemoteException(Exception): """ Base exception for RPC. This exception is raised when a problem occurs in the network layer. """ def __init__(self, message="", cause=None): """ Initializes a new RemoteException. Arguments: message -- The message accompanying this exception. cause -- The underlying cause of this exception. """ self._message = message self._cause = cause def __str__(self): return repr(self._message) def get_message(self): return self._message def get_cause(self): return self._cause class InvocationException(RemoteException): """ Exception raised when a problem occurs during the remote invocation of a method. """ pass class AuthorizationException(RemoteException): """ Exception raised when the caller is not authorized to invoke the remote method. """ pass class TimeoutException(Exception): """ Exception raised when the synchronous execution of a method takes longer than the given threshold because an underlying asynchronous reply did not arrive in time. """ pass class Endpoint(metaclass=abc.ABCMeta): """ The Endpoint class is an abstract base class for all objects participating in an RPC-enabled XMPP network. A user subclassing this class is required to implement the method: FQN(self) where FQN stands for Fully Qualified Name, an unambiguous name which specifies which object an RPC call refers to. It is the first part in a RPC method name '.'. """ def __init__(self, session, target_jid): """ Initialize a new Endpoint. This constructor should never be invoked by a user, instead it will be called by the factories which instantiate the RPC-enabled objects, of which only the classes are provided by the user. Arguments: session -- An RPC session instance. target_jid -- the identity of the remote XMPP entity. """ self.session = session self.target_jid = target_jid @abc.abstractproperty def FQN(self): return NotImplemented def get_methods(self): """ Returns a dictionary of all RPC method names provided by this class. This method returns the actual method names as found in the class definition which have been decorated with: @remote def some_rpc_method(arg1, arg2) Unless: (1) the name has been remapped, in which case the new name will be returned. @remote("new_name") def some_rpc_method(arg1, arg2) (2) the method is set to hidden @remote(False) def some_hidden_method(arg1, arg2) """ result = dict() for function in dir(self): test_attr = getattr(self, function, None) try: if test_attr._rpc: result[test_attr._rpc_name] = test_attr except Exception: pass return result class Proxy(Endpoint): """ Implementation of the Proxy pattern which is intended to wrap around Endpoints in order to intercept calls, marshall them and forward them to the remote object. """ def __init__(self, endpoint): """ Initializes a new Proxy. Arguments: endpoint -- The endpoint which is proxified. """ self._endpoint = endpoint def __getattribute__(self, name, *args): if name in ('__dict__', '_endpoint'): return object.__getattribute__(self, name) else: attribute = self._endpoint.__getattribute__(name) if hasattr(attribute, '__call__'): try: if attribute._rpc: async def _remote_call(*args, **kwargs): log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args) return await self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), *args, **kwargs) return _remote_call except: pass # If the attribute doesn't exist, don't care! return attribute def get_endpoint(self): """ Returns the proxified endpoint. """ return self._endpoint def FQN(self): return self._endpoint.FQN() class JabberRPCEntry(object): def __init__(self, endpoint_FQN, call): self._endpoint_FQN = endpoint_FQN self._call = call def call_method(self, args): return_value = self._call(*args) if return_value is None: return return_value else: return self._return(return_value) def get_endpoint_FQN(self): return self._endpoint_FQN def _return(self, *args): return args class RemoteSession(object): """ A context object for a Jabber-RPC session. """ def __init__(self, client, session_close_callback): """ Initializes a new RPC session. Arguments: client -- The Slixmpp client associated with this session. session_close_callback -- A callback called when the session is closed. """ self._client = client self._session_close_callback = session_close_callback self._event = asyncio.Event() self._entries = {} self._acls = {} async def _wait(self): await self._event.wait() def _notify(self, event): log.debug("RPC Session as %s started.", self._client.boundjid.full) self._client.send_presence() self._event.set() def _register_call(self, endpoint, method, name=None): """ Registers a method from an endpoint as remotely callable. """ if name is None: name = method.__name__ key = "%s.%s" % (endpoint, name) log.debug("Registering call handler for %s (%s).", key, method) if key in self._entries: raise KeyError("A handler for %s has already been regisered!" % endpoint) self._entries[key] = JabberRPCEntry(endpoint, method) return key def _register_acl(self, endpoint, acl): log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint) self._acls[endpoint] = acl def _unregister_call(self, key): #removes the registered call if self._entries[key]: del self._entries[key] else: raise ValueError() def new_proxy(self, target_jid, endpoint_cls): """ Instantiates a new proxy object, which proxies to a remote endpoint. This method uses a class reference without constructor arguments to instantiate the proxy. Arguments: target_jid -- the XMPP entity ID hosting the endpoint. endpoint_cls -- The remote (duck) type. """ try: argspec = inspect.getfullargspec(endpoint_cls.__init__) args = [None] * (len(argspec[0]) - 1) result = endpoint_cls(*args) Endpoint.__init__(result, self, target_jid) return Proxy(result) except: traceback.print_exc(file=sys.stdout) def new_handler(self, acl, handler_cls, *args, **kwargs): """ Instantiates a new handler object, which is called remotely by others. The user can control the effect of the call by implementing the remote method in the local endpoint class. The returned reference can be called locally and will behave as a regular instance. Arguments: acl -- Access control list (see ACL class) handler_clss -- The local (duck) type. *args -- Constructor arguments for the local type. **kwargs -- Constructor keyworded arguments for the local type. """ argspec = inspect.getfullargspec(handler_cls.__init__) base_argspec = inspect.getfullargspec(Endpoint.__init__) if(argspec == base_argspec): result = handler_cls(self, self._client.boundjid.full) else: result = handler_cls(*args, **kwargs) Endpoint.__init__(result, self, self._client.boundjid.full) method_dict = result.get_methods() for method_name, method in method_dict.items(): #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) self._register_call(result.FQN(), method, method_name) self._register_acl(result.FQN(), acl) return result async def _call_remote(self, pto, pmethod, *arguments): iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments)) try: result = await iq.send() fault = result['rpc_query']['method_response']['fault'] if fault: self._on_jabber_rpc_method_fault(result) except IqError as exc: self._on_jabber_rpc_error(exc.iq) args = xml2py(result['rpc_query']['method_response']['params']) if(len(args) > 0): return args return None def close(self, wait=False): """ Closes this session. """ self._client.disconnect(wait=wait) self._session_close_callback() def _on_jabber_rpc_method_call(self, iq): iq.enable('rpc_query') params = iq['rpc_query']['method_call']['params'] args = xml2py(params) pmethod = iq['rpc_query']['method_call']['method_name'] try: entry = self._entries[pmethod] rules = self._acls[entry.get_endpoint_FQN()] if ACL.check(rules, iq['from'], pmethod): return_value = entry.call_method(args) else: raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from'])) if return_value is None: return_value = () response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value)) response.send() except InvocationException as ie: fault = dict() fault['code'] = 500 fault['string'] = ie.get_message() self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault)) except AuthorizationException as ae: log.error(ae.get_message()) error = self._client.plugin['xep_0009']._forbidden(iq) error.send() except Exception as e: if isinstance(e, KeyError): log.error("No handler available for %s!", pmethod) error = self._client.plugin['xep_0009']._item_not_found(iq) else: traceback.print_exc(file=sys.stderr) log.error("An unexpected problem occurred invoking method %s!", pmethod) error = self._client.plugin['xep_0009']._undefined_condition(iq) error.send() def _on_jabber_rpc_method_fault(self, iq): iq.enable('rpc_query') fault = xml2fault(iq['rpc_query']['method_response']['fault']) raise InvocationException(fault['string']) def _on_jabber_rpc_error(self, iq): pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query']) condition = iq['error']['condition'] e = { 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])), 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])), 'undefined-condition': RemoteException("An unexpected problem occurred trying to invoke %s at %s!" % (pmethod, iq['from'])), }.get(condition) if e is None: e = RemoteException("An unexpected exception occurred at %s!" % iq['from']) raise e class Remote: """ Bootstrap class for Jabber-RPC sessions. New sessions are openend with an existing XMPP client, or one is instantiated on demand. """ _sessions = dict() @classmethod async def new_session_with_client(cls, client: ClientXMPP) -> RemoteSession: """ Opens a new session with a given client. :param client: An XMPP client. """ if client.boundjid.bare in cls._sessions: raise RemoteException("There already is a session associated with these credentials!") else: cls._sessions[client.boundjid.bare] = client def _session_close_callback(): del cls._sessions[client.boundjid.bare] result = RemoteSession(client, _session_close_callback) client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call) start_event_handler = result._notify client.add_event_handler("session_start", start_event_handler) client.connect() done, pending = await asyncio.wait([result._wait()], timeout=30) if pending: raise RemoteException("Could not connect to XMPP server in 30 seconds!") return result @classmethod async def new_session(cls, jid: JID, password: str) -> RemoteSession: """ Opens a new session and instantiates a new XMPP client. :param jid: The XMPP JID for logging in. :param password: The password for logging in. """ client = slixmpp.ClientXMPP(jid, password) #? Register plug-ins. client.register_plugin('xep_0004') # Data Forms client.register_plugin('xep_0009') # Jabber-RPC client.register_plugin('xep_0030') # Service Discovery client.register_plugin('xep_0060') # PubSub client.register_plugin('xep_0199') # XMPP Ping return await cls.new_session_with_client(client) slixmpp/slixmpp/plugins/xep_0009/rpc.py000066400000000000000000000122601477105560000203370ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Iq from slixmpp.xmlstream import ET, register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0009 import stanza from slixmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse log = logging.getLogger(__name__) class XEP_0009(BasePlugin): name = 'xep_0009' description = 'XEP-0009: Jabber-RPC' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, RPCQuery) register_stanza_plugin(RPCQuery, MethodCall) register_stanza_plugin(RPCQuery, MethodResponse) self.xmpp.register_handler( Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)), self._handle_method_call) ) self.xmpp.register_handler( Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)), self._handle_method_response) ) #self.activeCalls = [] self.xmpp['xep_0030'].add_feature('jabber:iq:rpc') self.xmpp['xep_0030'].add_identity('automation','rpc') def make_iq_method_call(self, pto, pmethod, params): iq = self.xmpp.make_iq_set() iq['to'] = pto iq['from'] = self.xmpp.boundjid.full iq.enable('rpc_query') iq['rpc_query']['method_call']['method_name'] = pmethod iq['rpc_query']['method_call']['params'] = params return iq def make_iq_method_response(self, pid, pto, params): iq = self.xmpp.make_iq_result(pid) iq['to'] = pto iq['from'] = self.xmpp.boundjid.full iq.enable('rpc_query') iq['rpc_query']['method_response']['params'] = params return iq def make_iq_method_response_fault(self, pid, pto, params): iq = self.xmpp.make_iq_result(pid) iq['to'] = pto iq['from'] = self.xmpp.boundjid.full iq.enable('rpc_query') iq['rpc_query']['method_response']['params'] = None iq['rpc_query']['method_response']['fault'] = params return iq # def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition): # iq = self.xmpp.make_iq_error(pid) # iq.attrib['to'] = pto # iq.attrib['from'] = self.xmpp.boundjid.full # iq['error']['code'] = code # iq['error']['type'] = type # iq['error']['condition'] = condition # iq['rpc_query']['method_call']['method_name'] = pmethod # iq['rpc_query']['method_call']['params'] = params # return iq def _item_not_found(self, iq): payload = iq.get_payload() iq = iq.reply() iq.error().set_payload(payload) iq['error']['code'] = '404' iq['error']['type'] = 'cancel' iq['error']['condition'] = 'item-not-found' return iq def _undefined_condition(self, iq): payload = iq.get_payload() iq = iq.reply() iq.error().set_payload(payload) iq['error']['code'] = '500' iq['error']['type'] = 'cancel' iq['error']['condition'] = 'undefined-condition' return iq def _forbidden(self, iq): payload = iq.get_payload() iq = iq.reply() iq.error().set_payload(payload) iq['error']['code'] = '403' iq['error']['type'] = 'auth' iq['error']['condition'] = 'forbidden' return iq def _recipient_unvailable(self, iq): payload = iq.get_payload() iq = iq.reply() iq.error().set_payload(payload) iq['error']['code'] = '404' iq['error']['type'] = 'wait' iq['error']['condition'] = 'recipient-unavailable' return iq def _handle_method_call(self, iq): type = iq['type'] if type == 'set': log.debug("Incoming Jabber-RPC call from %s", iq['from']) self.xmpp.event('jabber_rpc_method_call', iq) else: if type == 'error' and ['rpc_query'] is None: self.handle_error(iq) else: log.debug("Incoming Jabber-RPC error from %s", iq['from']) self.xmpp.event('jabber_rpc_error', iq) def _handle_method_response(self, iq): if iq['rpc_query']['method_response']['fault'] is not None: log.debug("Incoming Jabber-RPC fault from %s", iq['from']) #self._on_jabber_rpc_method_fault(iq) self.xmpp.event('jabber_rpc_method_fault', iq) else: log.debug("Incoming Jabber-RPC response from %s", iq['from']) self.xmpp.event('jabber_rpc_method_response', iq) def _send_fault(self, iq, fault_xml): # fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml) fault.send() def _extract_method(self, stanza): xml = ET.fromstring("%s" % stanza) return xml.find("./methodCall/methodName").text slixmpp/slixmpp/plugins/xep_0009/stanza.py000066400000000000000000000025211477105560000210520ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream.stanzabase import ElementBase from xml.etree import ElementTree as ET class RPCQuery(ElementBase): name = 'query' namespace = 'jabber:iq:rpc' plugin_attrib = 'rpc_query' class MethodCall(ElementBase): name = 'methodCall' namespace = 'jabber:iq:rpc' plugin_attrib = 'method_call' interfaces = {'method_name', 'params'} def get_method_name(self): return self._get_sub_text('methodName') def set_method_name(self, value): return self._set_sub_text('methodName', value) def get_params(self): return self.xml.find('{%s}params' % self.namespace) def set_params(self, params): self.append(params) class MethodResponse(ElementBase): name = 'methodResponse' namespace = 'jabber:iq:rpc' plugin_attrib = 'method_response' interfaces = {'params', 'fault'} def get_params(self): return self.xml.find('{%s}params' % self.namespace) def set_params(self, params): self.append(params) def get_fault(self) -> ET: return self.xml.find('{%s}fault' % self.namespace) def set_fault(self, fault: ET) -> None: self.append(fault) slixmpp/slixmpp/plugins/xep_0012/000077500000000000000000000000001477105560000171725ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0012/__init__.py000066400000000000000000000005571477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0012.stanza import LastActivity from slixmpp.plugins.xep_0012.last_activity import XEP_0012 register_plugin(XEP_0012) slixmpp/slixmpp/plugins/xep_0012/last_activity.py000066400000000000000000000136741477105560000224360ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from datetime import datetime, timedelta from typing import ( Dict, Optional ) from slixmpp.plugins import BasePlugin from slixmpp import JID from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0012 import stanza, LastActivity log = logging.getLogger(__name__) class XEP_0012(BasePlugin): """ XEP-0012 Last Activity """ name = 'xep_0012' description = 'XEP-0012: Last Activity' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, LastActivity) self._last_activities = {} self.xmpp.register_handler( CoroutineCallback('Last Activity', StanzaPath('iq@type=get/last_activity'), self._handle_get_last_activity)) self.api.register(self._default_get_last_activity, 'get_last_activity', default=True) self.api.register(self._default_set_last_activity, 'set_last_activity', default=True) self.api.register(self._default_del_last_activity, 'del_last_activity', default=True) def plugin_end(self): self.xmpp.remove_handler('Last Activity') self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('jabber:iq:last') def begin_idle(self, jid: Optional[JID] = None, status: Optional[str] = None) -> Future: """Reset the last activity for the given JID. .. versionchanged:: 1.8.0 This function now returns a Future. :param status: Optional status. """ return self.set_last_activity(jid, 0, status) def end_idle(self, jid: Optional[JID] = None) -> Future: """Remove the last activity of a JID. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.del_last_activity(jid) def start_uptime(self, status: Optional[str] = None) -> Future: """ .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.set_last_activity(None, 0, status) def set_last_activity(self, jid=None, seconds=None, status=None) -> Future: """Set last activity for a JID. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['set_last_activity'](jid, args={ 'seconds': seconds, 'status': status }) def del_last_activity(self, jid: JID) -> Future: """Remove the last activity of a JID. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['del_last_activity'](jid) def get_last_activity(self, jid: JID, local: bool = False, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Get last activity for a specific JID. :param local: Fetch the value from the local cache. """ if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full if local or jid in (None, ''): log.debug("Looking up local last activity data for %s", jid) return self.api['get_last_activity'](jid, None, ifrom, None) iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) iq.enable('last_activity') return iq.send(**iqkwargs) async def _handle_get_last_activity(self, iq: Iq): log.debug("Received last activity query from " + \ "<%s> to <%s>.", iq['from'], iq['to']) reply = await self.api['get_last_activity'](iq['to'], None, iq['from'], iq) reply.send() # ================================================================= # Default in-memory implementations for storing last activity data. # ================================================================= def _default_set_last_activity(self, jid: JID, node: str, ifrom: JID, data: Dict): seconds = data.get('seconds', None) if seconds is None: seconds = 0 status = data.get('status', None) if status is None: status = '' self._last_activities[jid] = { 'seconds': datetime.now() - timedelta(seconds=seconds), 'status': status} def _default_del_last_activity(self, jid: JID, node: str, ifrom: JID, data: Dict): if jid in self._last_activities: del self._last_activities[jid] def _default_get_last_activity(self, jid: JID, node: str, ifrom: JID, iq: Iq) -> Iq: if not isinstance(iq, Iq): reply = self.xmpp.Iq() else: reply = iq.reply() if jid not in self._last_activities: raise XMPPError('service-unavailable') bare = JID(jid).bare if bare != self.xmpp.boundjid.bare: if bare in self.xmpp.roster[jid]: sub = self.xmpp.roster[jid][bare]['subscription'] if sub not in ('from', 'both'): raise XMPPError('forbidden') td = datetime.now() - self._last_activities[jid]['seconds'] seconds = td.seconds + td.days * 24 * 3600 status = self._last_activities[jid]['status'] reply['last_activity']['seconds'] = seconds reply['last_activity']['status'] = status return reply slixmpp/slixmpp/plugins/xep_0012/stanza.py000066400000000000000000000013071477105560000210450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class LastActivity(ElementBase): name = 'query' namespace = 'jabber:iq:last' plugin_attrib = 'last_activity' interfaces = {'seconds', 'status'} def get_seconds(self): return int(self._get_attr('seconds')) def set_seconds(self, value): self._set_attr('seconds', str(value)) def get_status(self): return self.xml.text def set_status(self, value): self.xml.text = str(value) def del_status(self): self.xml.text = '' slixmpp/slixmpp/plugins/xep_0013/000077500000000000000000000000001477105560000171735ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0013/__init__.py000066400000000000000000000005421477105560000213050ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0013.stanza import Offline from slixmpp.plugins.xep_0013.offline import XEP_0013 register_plugin(XEP_0013) slixmpp/slixmpp/plugins/xep_0013/offline.py000066400000000000000000000075111477105560000211730ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio import logging from asyncio import Future from typing import Iterable, Optional, Callable, List, Set, Union from slixmpp import JID from slixmpp.stanza import Message, Iq from slixmpp.xmlstream.handler import Collector from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0013 import stanza log = logging.getLogger(__name__) class XEP_0013(BasePlugin): """ XEP-0013 Flexible Offline Message Retrieval """ name = 'xep_0013' description = 'XEP-0013: Flexible Offline Message Retrieval' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, stanza.Offline) register_stanza_plugin(Message, stanza.Offline) def get_count(self, **kwargs): return self.xmpp['xep_0030'].get_info( node='http://jabber.org/protocol/offline', local=False, **kwargs) def get_headers(self, **kwargs): return self.xmpp['xep_0030'].get_items( node='http://jabber.org/protocol/offline', local=False, **kwargs) def view(self, nodes: Iterable[str], ifrom: Optional[JID] = None, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: if not isinstance(nodes, (list, set)): nodes = [nodes] iq = self.xmpp.Iq() iq['type'] = 'get' iq['from'] = ifrom offline = iq['offline'] for node in nodes: item = stanza.Item() item['node'] = node item['action'] = 'view' offline.append(item) collector = Collector( 'Offline_Results_%s' % iq['id'], StanzaPath('message/offline')) self.xmpp.register_handler(collector) def wrapped_cb(iq): results = collector.stop() if iq['type'] == 'result': iq['offline']['results'] = results callback(iq) return iq.send(timeout=timeout, callback=wrapped_cb) def remove(self, nodes: Union[List[str], Set[str], str], ifrom: Optional[JID] = None, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: if not isinstance(nodes, (list, set)): nodes = [nodes] iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom offline = iq['offline'] for node in nodes: item = stanza.Item() item['node'] = node item['action'] = 'remove' offline.append(item) return iq.send(timeout=timeout, callback=callback) def fetch(self, ifrom: Optional[JID] = None, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['offline']['fetch'] = True collector = Collector( 'Offline_Results_%s' % iq['id'], StanzaPath('message/offline')) self.xmpp.register_handler(collector) def wrapped_cb(iq): results = collector.stop() if iq['type'] == 'result': iq['offline']['results'] = results callback(iq) return iq.send(timeout=timeout, callback=wrapped_cb) def purge(self, ifrom: Optional[JID] = None, timeout: Optional[int] = None, callback: Optional[Callable] = None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq['offline']['purge'] = True return iq.send(timeout=timeout, callback=callback) slixmpp/slixmpp/plugins/xep_0013/stanza.py000066400000000000000000000024041477105560000210450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Offline(ElementBase): name = 'offline' namespace = 'http://jabber.org/protocol/offline' plugin_attrib = 'offline' interfaces = {'fetch', 'purge', 'results'} bool_interfaces = interfaces def setup(self, xml=None): ElementBase.setup(self, xml) self._results = [] # The results interface is meant only as an easy # way to access the set of collected message responses # from the query. def get_results(self): return self._results def set_results(self, values): self._results = values def del_results(self): self._results = [] class Item(ElementBase): name = 'item' namespace = 'http://jabber.org/protocol/offline' plugin_attrib = 'item' interfaces = {'action', 'node', 'jid'} actions = {'view', 'remove'} def get_jid(self): return JID(self._get_attr('jid')) def set_jid(self, value): self._set_attr('jid', str(value)) register_stanza_plugin(Offline, Item, iterable=True) slixmpp/slixmpp/plugins/xep_0016/000077500000000000000000000000001477105560000171765ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0016/__init__.py000066400000000000000000000006201477105560000213050ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0016 import stanza from slixmpp.plugins.xep_0016.stanza import Privacy from slixmpp.plugins.xep_0016.privacy import XEP_0016 register_plugin(XEP_0016) slixmpp/slixmpp/plugins/xep_0016/privacy.py000066400000000000000000000107311477105560000212270ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from asyncio import Future from typing import Optional, Callable, Iterable from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0016 import stanza from slixmpp.plugins.xep_0016.stanza import Privacy, Item class XEP_0016(BasePlugin): name = 'xep_0016' description = 'XEP-0016: Privacy Lists' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, Privacy) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Privacy.namespace) def get_privacy_lists(self, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'get' iq.enable('privacy') return iq.send(timeout=timeout, callback=callback) def get_list(self, name: str, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy']['list']['name'] = name return iq.send(timeout=timeout, callback=callback) def get_active(self, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy'].enable('active') return iq.send(timeout=timeout, callback=callback) def get_default(self, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'get' iq['privacy'].enable('default') return iq.send(timeout=timeout, callback=callback) def activate(self, name: str, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['active']['name'] = name return iq.send(timeout=timeout, callback=callback) def deactivate(self, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy'].enable('active') return iq.send(timeout=timeout, callback=callback) def make_default(self, name, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['default']['name'] = name return iq.send(timeout=timeout, callback=callback) def remove_default(self, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy'].enable('default') return iq.send(timeout=timeout, callback=callback) def edit_list(self, name: str, rules: Iterable[Item], timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['list']['name'] = name priv_list = iq['privacy']['list'] if not rules: rules = [] for rule in rules: if isinstance(rule, Item): priv_list.append(rule) continue priv_list.add_item( rule['value'], rule['action'], rule['order'], itype=rule.get('type', None), iq=rule.get('iq', None), message=rule.get('message', None), presence_in=rule.get( 'presence_in', rule.get('presence-in', None) ), presence_out=rule.get( 'presence_out', rule.get('presence-out', None) ) ) return iq.send(timeout=timeout, callback=callback) def remove_list(self, name: str, timeout: Optional[int] = None, callback: Optional[Callable] = None) -> Future: iq = self.xmpp.Iq() iq['type'] = 'set' iq['privacy']['list']['name'] = name return iq.send(timeout=timeout, callback=callback) slixmpp/slixmpp/plugins/xep_0016/stanza.py000066400000000000000000000057741477105560000210650ustar00rootroot00000000000000from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Privacy(ElementBase): name = 'query' namespace = 'jabber:iq:privacy' plugin_attrib = 'privacy' interfaces = set() def add_list(self, name): priv_list = List() priv_list['name'] = name self.append(priv_list) return priv_list class Active(ElementBase): name = 'active' namespace = 'jabber:iq:privacy' plugin_attrib = name interfaces = {'name'} class Default(ElementBase): name = 'default' namespace = 'jabber:iq:privacy' plugin_attrib = name interfaces = {'name'} class List(ElementBase): name = 'list' namespace = 'jabber:iq:privacy' plugin_attrib = name plugin_multi_attrib = 'lists' interfaces = {'name'} def add_item(self, value, action, order, itype=None, iq=False, message=False, presence_in=False, presence_out=False): item = Item() item.values = {'type': itype, 'value': value, 'action': action, 'order': order, 'message': message, 'iq': iq, 'presence_in': presence_in, 'presence_out': presence_out} self.append(item) return item class Item(ElementBase): name = 'item' namespace = 'jabber:iq:privacy' plugin_attrib = name plugin_multi_attrib = 'items' interfaces = {'type', 'value', 'action', 'order', 'iq', 'message', 'presence_in', 'presence_out'} bool_interfaces = {'message', 'iq', 'presence_in', 'presence_out'} type_values = ('', 'jid', 'group', 'subscription') action_values = ('allow', 'deny') def set_type(self, value): if value and value not in self.type_values: raise ValueError('Unknown type value: %s' % value) else: self._set_attr('type', value) def set_action(self, value): if value not in self.action_values: raise ValueError('Unknown action value: %s' % value) else: self._set_attr('action', value) def set_presence_in(self, value): keep = True if value else False self._set_sub_text('presence-in', '', keep=keep) def get_presence_in(self): pres = self.xml.find('{%s}presence-in' % self.namespace) return pres is not None def del_presence_in(self): self._del_sub('{%s}presence-in' % self.namespace) def set_presence_out(self, value): keep = True if value else False self._set_sub_text('presence-in', '', keep=keep) def get_presence_out(self): pres = self.xml.find('{%s}presence-in' % self.namespace) return pres is not None def del_presence_out(self): self._del_sub('{%s}presence-in' % self.namespace) register_stanza_plugin(Privacy, Active) register_stanza_plugin(Privacy, Default) register_stanza_plugin(Privacy, List, iterable=True) register_stanza_plugin(List, Item, iterable=True) slixmpp/slixmpp/plugins/xep_0020/000077500000000000000000000000001477105560000171715ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0020/__init__.py000066400000000000000000000006471477105560000213110ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0020 import stanza from slixmpp.plugins.xep_0020.stanza import FeatureNegotiation from slixmpp.plugins.xep_0020.feature_negotiation import XEP_0020 register_plugin(XEP_0020) slixmpp/slixmpp/plugins/xep_0020/feature_negotiation.py000066400000000000000000000017441477105560000236040ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Iq, Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0020 import stanza, FeatureNegotiation from slixmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) class XEP_0020(BasePlugin): name = 'xep_0020' description = 'XEP-0020: Feature Negotiation' dependencies = {'xep_0004', 'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace) register_stanza_plugin(FeatureNegotiation, Form) register_stanza_plugin(Iq, FeatureNegotiation) register_stanza_plugin(Message, FeatureNegotiation) slixmpp/slixmpp/plugins/xep_0020/stanza.py000066400000000000000000000006071477105560000210460ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class FeatureNegotiation(ElementBase): name = 'feature' namespace = 'http://jabber.org/protocol/feature-neg' plugin_attrib = 'feature_neg' interfaces = set() slixmpp/slixmpp/plugins/xep_0027/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0027/__init__.py000066400000000000000000000005521477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0027.stanza import Signed, Encrypted from slixmpp.plugins.xep_0027.gpg import XEP_0027 register_plugin(XEP_0027) slixmpp/slixmpp/plugins/xep_0027/gpg.py000066400000000000000000000144201477105560000203300ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.thirdparty import GPG from asyncio import Future from slixmpp.stanza import Presence, Message from slixmpp.plugins.base 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_0027 import stanza, Signed, Encrypted def _extract_data(data, kind): stripped = [] begin_headers = False begin_data = False if isinstance(data, bytes): data = data.decode() for line in data.split('\n'): if not begin_headers and 'BEGIN PGP %s' % kind in line: begin_headers = True continue if begin_headers and line.strip() == '': begin_data = True continue if 'END PGP %s' % kind in line: return '\n'.join(stripped) if begin_data: stripped.append(line) return '' class XEP_0027(BasePlugin): """ XEP-0027: Current Jabber OpenPGP Usage """ name = 'xep_0027' description = 'XEP-0027: Current Jabber OpenPGP Usage' dependencies = set() stanza = stanza default_config = { 'gpg_binary': 'gpg', 'gpg_home': '', 'use_agent': True, 'keyring': None, 'key_server': 'pgp.mit.edu' } def plugin_init(self): self.gpg = GPG(gnupghome=self.gpg_home, gpgbinary=self.gpg_binary, use_agent=self.use_agent, keyring=self.keyring) self.xmpp.add_filter('out', self._sign_presence) self._keyids = {} self.api.register(self._set_keyid, 'set_keyid', default=True) self.api.register(self._get_keyid, 'get_keyid', default=True) self.api.register(self._del_keyid, 'del_keyid', default=True) self.api.register(self._get_keyids, 'get_keyids', default=True) register_stanza_plugin(Presence, Signed) register_stanza_plugin(Message, Encrypted) self.xmpp.add_event_handler('unverified_signed_presence', self._handle_unverified_signed_presence) self.xmpp.register_handler( Callback('Signed Presence', StanzaPath('presence/signed'), self._handle_signed_presence)) self.xmpp.register_handler( Callback('Encrypted Message', StanzaPath('message/encrypted'), self._handle_encrypted_message)) def plugin_end(self): self.xmpp.remove_handler('Encrypted Message') self.xmpp.remove_handler('Signed Presence') self.xmpp.del_filter('out', self._sign_presence) self.xmpp.del_event_handler('unverified_signed_presence', self._handle_unverified_signed_presence) def _sign_presence(self, stanza): if isinstance(stanza, Presence): if stanza['type'] == 'available' or \ stanza['type'] in Presence.showtypes: stanza['signed'] = stanza['status'] return stanza def sign(self, data, jid=None): keyid = self.get_keyid(jid) if keyid: signed = self.gpg.sign(data, keyid=keyid) return _extract_data(signed.data, 'SIGNATURE') def encrypt(self, data, jid=None): keyid = self.get_keyid(jid) if keyid: enc = self.gpg.encrypt(data, keyid) return _extract_data(enc.data, 'MESSAGE') def decrypt(self, data, jid=None): template = '-----BEGIN PGP MESSAGE-----\n' + \ '\n' + \ '%s\n' + \ '-----END PGP MESSAGE-----\n' dec = self.gpg.decrypt(template % data) return dec.data def verify(self, data, sig, jid=None): template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \ 'Hash: SHA1\n' + \ '\n' + \ '%s\n' + \ '-----BEGIN PGP SIGNATURE-----\n' + \ '\n' + \ '%s\n' + \ '-----END PGP SIGNATURE-----\n' v = self.gpg.verify(template % (data, sig)) return v def set_keyid(self, jid=None, keyid=None) -> Future: """Set a keyid for a specific JID. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['set_keyid'](jid, args=keyid) def get_keyid(self, jid=None) -> Future: """Get a keyid for a jid. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['get_keyid'](jid) def del_keyid(self, jid=None) -> Future: """Delete a keyid. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['del_keyid'](jid) def get_keyids(self) -> Future: """Get stored keyids. .. versionchanged:: 1.8.0 This function now returns a Future. """ return self.api['get_keyids']() def _handle_signed_presence(self, pres): self.xmpp.event('unverified_signed_presence', pres) def _handle_unverified_signed_presence(self, pres): verified = self.verify(pres['status'], pres['signed']) if verified.key_id: if not self.get_keyid(pres['from']): known_keyids = [e['keyid'] for e in self.gpg.list_keys()] if verified.key_id not in known_keyids: self.gpg.recv_keys(self.key_server, verified.key_id) self.set_keyid(jid=pres['from'], keyid=verified.key_id) self.xmpp.event('signed_presence', pres) def _handle_encrypted_message(self, msg): self.xmpp.event('encrypted_message', msg) # ================================================================= def _set_keyid(self, jid, node, ifrom, keyid): self._keyids[jid] = keyid def _get_keyid(self, jid, node, ifrom, keyid): return self._keyids.get(jid, None) def _del_keyid(self, jid, node, ifrom, keyid): if jid in self._keyids: del self._keyids[jid] def _get_keyids(self, jid, node, ifrom, data): return self._keyids slixmpp/slixmpp/plugins/xep_0027/stanza.py000066400000000000000000000024461477105560000210600ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class Signed(ElementBase): name = 'x' namespace = 'jabber:x:signed' plugin_attrib = 'signed' interfaces = {'signed'} is_extension = True def set_signed(self, value): parent = self.parent() xmpp = parent.stream data = xmpp['xep_0027'].sign(value, parent['from']) if data: self.xml.text = data else: del parent['signed'] def get_signed(self): return self.xml.text class Encrypted(ElementBase): name = 'x' namespace = 'jabber:x:encrypted' plugin_attrib = 'encrypted' interfaces = {'encrypted'} is_extension = True def set_encrypted(self, value): parent = self.parent() xmpp = parent.stream data = xmpp['xep_0027'].encrypt(value, parent['to']) if data: self.xml.text = data else: del parent['encrypted'] def get_encrypted(self): parent = self.parent() xmpp = parent.stream if self.xml.text: return xmpp['xep_0027'].decrypt(self.xml.text, parent['to']) return None slixmpp/slixmpp/plugins/xep_0030/000077500000000000000000000000001477105560000171725ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0030/__init__.py000066400000000000000000000007241477105560000213060ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0030 import stanza from slixmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems from slixmpp.plugins.xep_0030.static import StaticDisco from slixmpp.plugins.xep_0030.disco import XEP_0030 register_plugin(XEP_0030) slixmpp/slixmpp/plugins/xep_0030/disco.py000066400000000000000000000763031477105560000206560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from asyncio import Future from typing import ( Optional, Callable, List, Union, ) from slixmpp import JID from slixmpp.stanza import Iq from slixmpp.types import OptJid from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems from slixmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) class XEP_0030(BasePlugin): """ XEP-0030: Service Discovery Service discovery in XMPP allows entities to discover information about other agents in the network, such as the feature sets supported by a client, or signposts to other, related entities. Also see . The XEP-0030 plugin works using a hierarchy of dynamic node handlers, ranging from global handlers to specific JID+node handlers. The default set of handlers operate in a static manner, storing disco information in memory. However, custom handlers may use any available backend storage mechanism desired, such as SQLite or Redis. Node handler hierarchy: ====== ======= ============================ JID Node Level ====== ======= ============================ None None Global Given None All nodes for the JID None Given Node on self.xmpp.boundjid Given Given A single node ====== ======= ============================ Adding information for a given node without specifying the JID will use the bound JID and therefore must be done after the bind happens. Stream Handlers: :: Disco Info -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#info. Disco Items -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#items. Events: - :term:`disco_info` -- Received a disco#info Iq query result. - :term:`disco_items` -- Received a disco#items Iq query result. Attributes: :var static: Object containing the default set of static node handlers. """ name = 'xep_0030' description = 'XEP-0030: Service Discovery' dependencies = set() stanza = stanza default_config = { 'use_cache': True, 'wrap_results': False } static: StaticDisco def plugin_init(self): """ Start the XEP-0030 plugin. """ self.xmpp.register_handler(CoroutineCallback( 'Disco Info', StanzaPath('iq/disco_info'), self._handle_disco_info )) self.xmpp.register_handler(CoroutineCallback( 'Disco Items', StanzaPath('iq/disco_items'), self._handle_disco_items )) register_stanza_plugin(Iq, DiscoInfo) register_stanza_plugin(Iq, DiscoItems) self.static = StaticDisco(self.xmpp, self) self._disco_ops = [ 'get_info', 'set_info', 'set_identities', 'set_features', 'get_items', 'set_items', 'del_items', 'add_identity', 'del_identity', 'add_feature', 'del_feature', 'add_item', 'del_item', 'del_identities', 'del_features', 'cache_info', 'get_cached_info', 'supports', 'has_identity'] for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) self.domain_infos = {} def session_bind(self, jid): self.add_feature('http://jabber.org/protocol/disco#info') def plugin_end(self): self.del_feature('http://jabber.org/protocol/disco#info') def _add_disco_op(self, op, default_handler): self.api.register(default_handler, op) self.api.register_default(default_handler, op) def set_node_handler(self, htype: str, jid: OptJid = None, node: Optional[str] = None, handler: Optional[Callable] = None): """ Add a node handler for the given hierarchy level and handler type. Node handlers are ordered in a hierarchy where the most specific handler is executed. Thus, a fallback, global handler can be used for the majority of cases with a few node specific handler that override the global behavior. Node handler hierarchy: ====== ======= ============================ JID Node Level ====== ======= ============================ None None Global Given None All nodes for the JID None Given Node on self.xmpp.boundjid Given Given A single node ====== ======= ============================ Handler types: :: get_info get_items set_identities set_features set_items del_items del_identities del_identity del_feature del_features del_item add_identity add_feature add_item :param htype: The operation provided by the handler. :param jid: The JID the handler applies to. May be narrowed further if a node is given. :param node: The particular node the handler is for. If no JID is given, then the self.xmpp.boundjid.full is assumed. :param handler: The handler function to use. """ self.api.register(handler, htype, jid, node) def del_node_handler(self, htype: str, jid: OptJid, node: Optional[str]): """ Remove a handler type for a JID and node combination. The next handler in the hierarchy will be used if one exists. If removing the global handler, make sure that other handlers exist to process existing nodes. Node handler hierarchy: ====== ======= ============================ JID Node Level ====== ======= ============================ None None Global Given None All nodes for the JID None Given Node on self.xmpp.boundjid Given Given A single node ====== ======= ============================ :param htype: The type of handler to remove. :param jid: The JID from which to remove the handler. :param node: The node from which to remove the handler. """ self.api.unregister(htype, jid, node) def restore_defaults(self, jid: OptJid = None, node: Optional[str] = None, handlers: Optional[List[Callable]] = None): """ Change all or some of a node's handlers to the default handlers. Useful for manually overriding the contents of a node that would otherwise be handled by a JID level or global level dynamic handler. The default is to use the built-in static handlers, but that may be changed by modifying self.default_handlers. :param jid: The JID owning the node to modify. :param node: The node to change to using static handlers. :param handlers: Optional list of handlers to change to the default version. If provided, only these handlers will be changed. Otherwise, all handlers will use the default version. """ if handlers is None: handlers = self._disco_ops for op in handlers: self.api.restore_default(op, jid, node) def supports(self, jid: OptJid = None, node: Optional[str] = None, feature: Optional[str] = None, local: bool = False, cached: bool = True, ifrom: OptJid = None) -> Future: """ Check if a JID supports a given feature. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: Request info from this JID. :param node: The particular node to query. :param feature: The name of the feature to check. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. :param cached: If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. :returns True: The feature is supported :returns False: The feature is not listed as supported :returns None: Nothing could be found due to a timeout """ data = {'feature': feature, 'local': local, 'cached': cached} return self.api['supports'](jid, node, ifrom, data) def has_identity(self, jid: OptJid = None, node: Optional[str] = None, category: Optional[str] = None, itype: Optional[str] = None, lang: Optional[str] = None, local: bool = False, cached: bool = True, ifrom: OptJid = None) -> Future: """ Check if a JID provides a given identity. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: Request info from this JID. :param node: The particular node to query. :param category: The category of the identity to check. :param itype: The type of the identity to check. :param lang: The language of the identity to check. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. :param cached: If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. :returns True: The identity is provided :returns False: The identity is not listed :returns None: Nothing could be found due to a timeout """ data = {'category': category, 'itype': itype, 'lang': lang, 'local': local, 'cached': cached} return self.api['has_identity'](jid, node, ifrom, data) async def get_info_from_domain(self, domain=None, timeout=None, cached=True, callback=None, **iqkwargs): """Fetch disco#info of specified domain and one disco#items level below """ if domain is None: domain = self.xmpp.boundjid.domain if not cached or domain not in self.domain_infos: infos = [asyncio.create_task(self.get_info( domain, timeout=timeout, **iqkwargs))] iq_items = await self.get_items( domain, timeout=timeout, **iqkwargs) items = iq_items['disco_items']['items'] infos += [ asyncio.create_task(self.get_info(item[0], timeout=timeout, **iqkwargs)) for item in items] info_futures, _ = await asyncio.wait( infos, timeout=timeout, ) self.domain_infos[domain] = [ future.result() for future in info_futures if not future.exception() ] results = self.domain_infos[domain] if callback is not None: callback(results) return results async def get_info(self, jid: OptJid = None, node: Optional[str] = None, local: Optional[bool] = None, cached: Optional[bool] = None, **kwargs) -> Iq: """ Retrieve the disco#info results from a given JID/node combination. Info may be retrieved from both local resources and remote agents; the local parameter indicates if the information should be gathered by executing the local node handlers, or if a disco#info stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoInfo stanza will be returned. Otherwise, an Iq stanza will be returned. .. versionchanged:: 1.8.0 This function is now a coroutine. :param jid: Request info from this JID. :param node: The particular node to query. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remote JID to retrieve the info. :param cached: If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ if local is None: if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full elif jid in (None, ''): local = True ifrom = kwargs.pop('ifrom', None) if self.xmpp.is_component and ifrom is None: ifrom = self.xmpp.boundjid if local: log.debug("Looking up local disco#info data " "for %s, node %s.", jid, node) info = await self.api['get_info']( jid, node, ifrom, kwargs ) info = self._fix_default_info(info) return self._wrap(ifrom, jid, info) if cached: log.debug("Looking up cached disco#info data " "for %s, node %s.", jid, node) info = await self.api['get_cached_info']( jid, node, ifrom, kwargs ) if info is not None: return self._wrap(ifrom, jid, info) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = ifrom or kwargs.get('dfrom', '') iq['to'] = jid iq['type'] = 'get' iq['disco_info']['node'] = node if node else '' return await iq.send(**kwargs) def set_info(self, jid: OptJid = None, node: Optional[str] = None, info: Optional[Union[Iq, DiscoInfo]] = None) -> Future: """ Set the disco#info data for a JID/node based on an existing disco#info stanza. .. versionchanged:: 1.8.0 This function now returns a Future. """ if isinstance(info, Iq): info = info['disco_info'] return self.api['set_info'](jid, node, None, info) async def get_items(self, jid: OptJid = None, node: Optional[str] = None, local: bool = False, ifrom: OptJid = None, **kwargs) -> Iq: """ Retrieve the disco#items results from a given JID/node combination. Items may be retrieved from both local resources and remote agents; the local parameter indicates if the items should be gathered by executing the local node handlers, or if a disco#items stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoItems stanza will be returned. Otherwise, an Iq stanza will be returned. .. versionchanged:: 1.8.0 This function is now a coroutine. :param jid: Request info from this JID. :param node: The particular node to query. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. :param iterator: If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ if ifrom is None and self.xmpp.is_component: ifrom = self.xmpp.boundjid.bare if local or local is None and jid is None: items = await self.api['get_items'](jid, node, ifrom, kwargs) return self._wrap(ifrom, jid, items) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = ifrom or kwargs.get('dfrom', '') iq['to'] = jid iq['type'] = 'get' iq['disco_items']['node'] = node if node else '' if kwargs.get('iterator', False) and self.xmpp['xep_0059']: return self.xmpp['xep_0059'].iterate(iq, 'disco_items') else: return await iq.send(**kwargs) def set_items(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Set or replace all items for the specified JID/node combination. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name). .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: Optional node to modify. :param items: A series of items in tuple format. """ return self.api['set_items'](jid, node, None, kwargs) def del_items(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove all items from the given JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. Arguments: :param jid: The JID to modify. :param node: Optional node to modify. """ return self.api['del_items'](jid, node, None, kwargs) def add_item(self, jid: str = '', name: str = '', node: Optional[str] = None, subnode: str = '', ijid: OptJid = None) -> Future: """ Add a new item element to the given JID/node combination. Each item is required to have a JID, but may also specify a node value to reference non-addressable entities. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID for the item. :param name: Optional name for the item. :param node: The node to modify. :param subnode: Optional node for the item. :param ijid: The JID to modify. """ if not jid: jid = self.xmpp.boundjid.full kwargs = {'ijid': jid, 'name': name, 'inode': subnode} return self.api['add_item'](ijid, node, None, kwargs) def del_item(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove a single item from the given JID/node combination. :param jid: The JID to modify. :param node: The node to modify. :param ijid: The item's JID. :param inode: The item's node. """ return self.api['del_item'](jid, node, None, kwargs) def add_identity(self, category: str = '', itype: str = '', name: str = '', node: Optional[str] = None, jid: OptJid = None, lang: Optional[str] = None) -> Future: """ Add a new identity to the given JID/node combination. Each identity must be unique in terms of all four identity components: category, type, name, and language. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. A category and type is always required. .. versionchanged:: 1.8.0 This function now returns a Future. :param category: The identity's category. :param itype: The identity's type. :param name: Optional name for the identity. :param lang: Optional two-letter language code. :param node: The node to modify. :param jid: The JID to modify. """ kwargs = {'category': category, 'itype': itype, 'name': name, 'lang': lang} return self.api['add_identity'](jid, node, None, kwargs) def add_feature(self, feature: str, node: Optional[str] = None, jid: OptJid = None) -> Future: """ Add a feature to a JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. :param feature: The namespace of the supported feature. :param node: The node to modify. :param jid: The JID to modify. """ kwargs = {'feature': feature} return self.api['add_feature'](jid, node, None, kwargs) def del_identity(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove an identity from the given JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param category: The identity's category. :param itype: The identity's type value. :param name: Optional, human readable name for the identity. :param lang: Optional, the identity's xml:lang value. """ return self.api['del_identity'](jid, node, None, kwargs) def del_feature(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove a feature from a given JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param feature: The feature's namespace. """ return self.api['del_feature'](jid, node, None, kwargs) def set_identities(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Add or replace all identities for the given JID/node combination. The identities must be in a set where each identity is a tuple of the form: (category, type, lang, name) .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param identities: A set of identities in tuple form. :param lang: Optional, xml:lang value. """ return self.api['set_identities'](jid, node, None, kwargs) def del_identities(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove all identities for a JID/node combination. If a language is specified, only identities using that language will be removed. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param lang: Optional. If given, only remove identities using this xml:lang value. """ return self.api['del_identities'](jid, node, None, kwargs) def set_features(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Add or replace the set of supported features for a JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param features: The new set of supported features. """ return self.api['set_features'](jid, node, None, kwargs) def del_features(self, jid: OptJid = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove all features from a JID/node combination. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. """ return self.api['del_features'](jid, node, None, kwargs) async def _run_node_handler(self, htype, jid, node: Optional[str] = None, ifrom: OptJid = None, data=None): """ Execute the most specific node handler for the given JID/node combination. :param htype: The handler type to execute. :param jid: The JID requested. :param node: The node requested. :param data: Optional, custom data to pass to the handler. """ if not data: data = {} return await self.api[htype](jid, node, ifrom, data) async def _handle_disco_info(self, iq: Iq): """ Process an incoming disco#info stanza. If it is a get request, find and return the appropriate identities and features. If it is an info result, fire the disco_info event. :param iq: The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco info query from " "<%s> to <%s>.", iq['from'], iq['to']) info = await self.api['get_info'](iq['to'], iq['disco_info']['node'], iq['from'], iq) if isinstance(info, Iq): info['id'] = iq['id'] info.send() else: node = iq['disco_info']['node'] iq = iq.reply() if info: info = self._fix_default_info(info) info['node'] = node iq.set_payload(info.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco info result from " "<%s> to <%s>.", iq['from'], iq['to']) if self.use_cache: log.debug("Caching disco info result from " "<%s> to <%s>.", iq['from'], iq['to']) if self.xmpp.is_component: ito = iq['to'].full else: ito = None await self.api['cache_info'](iq['from'], iq['disco_info']['node'], ito, iq) self.xmpp.event('disco_info', iq) async def _handle_disco_items(self, iq: Iq): """ Process an incoming disco#items stanza. If it is a get request, find and return the appropriate items. If it is an items result, fire the disco_items event. :param iq: The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco items query from " "<%s> to <%s>.", iq['from'], iq['to']) items = await self.api['get_items'](iq['to'], iq['disco_items']['node'], iq['from'], iq) if isinstance(items, Iq): items.send() else: iq = iq.reply() if items: iq.set_payload(items.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco items result from " "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_items', iq) def _fix_default_info(self, info: DiscoInfo): """ Disco#info results for a JID are required to include at least one identity and feature. As a default, if no other identity is provided, Slixmpp will use either the generic component or the bot client identity. A the standard disco#info feature will also be added if no features are provided. :param info: The disco#info quest (not the full Iq stanza) to modify. """ result = info if isinstance(info, Iq): info = info['disco_info'] if not info['node']: if not info['identities']: if self.xmpp.is_component: log.debug("No identity found for this entity. " "Using default component identity.") info.add_identity('component', 'generic') else: log.debug("No identity found for this entity. " "Using default client identity.") info.add_identity('client', 'bot') if not info['features']: log.debug("No features found for this entity. " "Using default disco#info feature.") info.add_feature(info.namespace) return result def _wrap(self, ito: OptJid, ifrom: OptJid, payload, force=False) -> Iq: """ Ensure that results are wrapped in an Iq stanza if self.wrap_results has been set to True. :param ito: The JID to use as the 'to' value :param ifrom: The JID to use as the 'from' value :param payload: The disco data to wrap :param force: Force wrapping, regardless of self.wrap_results """ if (force or self.wrap_results) and not isinstance(payload, Iq): iq = self.xmpp.Iq() # Since we're simulating a result, we have to treat # the 'from' and 'to' values opposite the normal way. iq['to'] = self.xmpp.boundjid if ito is None else ito iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom iq['type'] = 'result' iq.append(payload) return iq return payload slixmpp/slixmpp/plugins/xep_0030/stanza/000077500000000000000000000000001477105560000204725ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0030/stanza/__init__.py000066400000000000000000000004441477105560000226050ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo from slixmpp.plugins.xep_0030.stanza.items import DiscoItems slixmpp/slixmpp/plugins/xep_0030/stanza/info.py000066400000000000000000000256201477105560000220040ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import ( Iterable, List, Optional, Set, Tuple, Union, Dict, ) from slixmpp.xmlstream import ElementBase, ET IdentityType = Tuple[str, str, Optional[str], Optional[str]] class DiscoInfo(ElementBase): """ XMPP allows for users and agents to find the identities and features supported by other entities in the XMPP network through service discovery, or "disco". In particular, the "disco#info" query type for stanzas is used to request the list of identities and features offered by a JID. An identity is a combination of a category and type, such as the 'client' category with a type of 'pc' to indicate the agent is a human operated client with a GUI, or a category of 'gateway' with a type of 'aim' to identify the agent as a gateway for the legacy AIM protocol. See `XMPP Registrar Disco Categories`_ for a full list of accepted category and type combinations. .. _XMPP Registrar Disco Categories: Features are simply a set of the namespaces that identify the supported features. For example, a client that supports service discovery will include the feature ``http://jabber.org/protocol/disco#info``. Since clients and components may operate in several roles at once, identity and feature information may be grouped into "nodes". If one were to write all of the identities and features used by a client, then node names would be like section headings. Example disco#info stanza: .. code-block:: xml """ name = 'query' namespace = 'http://jabber.org/protocol/disco#info' plugin_attrib = 'disco_info' #: Stanza interfaces: #: #: - ``node``: The name of the node to either query or return the info from #: - ``identities``: A set of 4-tuples, where each tuple contains the #: category, type, xml:lang and name of an identity #: - ``features``: A set of namespaces for features #: interfaces = {'node', 'features', 'identities'} lang_interfaces = {'identities'} # Cache identities and features _identities: Set[Tuple[str, str, Optional[str]]] _features: Set[str] def setup(self, xml: Optional[ET.ElementTree] = None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches identity and feature information. :param xml: Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._identities = {id[0:3] for id in self['identities']} self._features = self['features'] def add_identity(self, category: str, itype: str, name: Optional[str] = None, lang: Optional[str] = None ) -> bool: """ Add a new identity element. Each identity must be unique in terms of all four identity components. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. In any case, a category and type are required. :param category: The general category to which the agent belongs. :param itype: A more specific designation with the category. :param name: Optional human readable name for this identity. :param lang: Optional standard xml:lang value. """ identity = (category, itype, lang) if identity not in self._identities: self._identities.add(identity) id_xml = ET.Element('{%s}identity' % self.namespace) id_xml.attrib['category'] = category id_xml.attrib['type'] = itype if lang: id_xml.attrib['{%s}lang' % self.xml_ns] = lang if name: id_xml.attrib['name'] = name self.xml.insert(0, id_xml) return True return False def del_identity(self, category: str, itype: str, name=None, lang: Optional[str] = None) -> bool: """ Remove a given identity. :param category: The general category to which the agent belonged. :param itype: A more specific designation with the category. :param name: Optional human readable name for this identity. :param lang: Optional, standard xml:lang value. """ identity = (category, itype, lang) if identity in self._identities: self._identities.remove(identity) for id_xml in self.xml.findall('{%s}identity' % self.namespace): id = (id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None)) if id == identity: self.xml.remove(id_xml) return True return False def dict_identities(self, lang: Optional[str] = None) -> List[Dict[str, str]]: """ Return the list of all identities, each one as a dict with category, type, xml_lang, and name keys. :param lang: If there is a need to filter identities by lang. """ ids = self.get_identities(lang=lang, dedupe=True) dict_ids = [] for identity in ids: dict_ids.append({ 'category': identity[0], 'type': identity[1], 'xml_lang': identity[2], 'name': identity[3], }) return dict_ids def get_identities(self, lang: Optional[str] = None, dedupe: bool = True ) -> Iterable[IdentityType]: """ Return a set of all identities in tuple form as so: (category, type, lang, name) If a language was specified, only return identities using that language. :param lang: Optional, standard xml:lang value. :param dedupe: If True, de-duplicate identities, otherwise return a list of all identities. """ identities: Union[List[IdentityType], Set[IdentityType]] if dedupe: identities = set() else: identities = [] for id_xml in self.xml.findall('{%s}identity' % self.namespace): xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) category = id_xml.attrib.get('category', None) type_ = id_xml.attrib.get('type', None) name = id_xml.attrib.get('name', None) if lang is None or xml_lang == lang: id = (category, type_, xml_lang, name) if isinstance(identities, set): identities.add(id) else: identities.append(id) return identities def set_identities(self, identities: Iterable[IdentityType], lang: Optional[str] = None): """ Add or replace all identities. The identities must be a in set where each identity is a tuple of the form: (category, type, lang, name) If a language is specifified, any identities using that language will be removed to be replaced with the given identities. .. note:: An identity's language will not be changed regardless of the value of lang. :param identities: A set of identities in tuple form. :param lang: Optional, standard xml:lang value. """ self.del_identities(lang) for identity in identities: category, itype, lang, name = identity self.add_identity(category, itype, name, lang) def del_identities(self, lang: Optional[str] = None): """ Remove all identities. If a language was specified, only remove identities using that language. :param lang: Optional, standard xml:lang value. """ for id_xml in self.xml.findall('{%s}identity' % self.namespace): if lang is None: self.xml.remove(id_xml) elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang: self._identities.remove(( id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None))) self.xml.remove(id_xml) def add_feature(self, feature: str) -> bool: """ Add a single, new feature. :param feature: The namespace of the supported feature. """ if feature not in self._features: self._features.add(feature) feature_xml = ET.Element('{%s}feature' % self.namespace) feature_xml.attrib['var'] = feature self.xml.append(feature_xml) return True return False def del_feature(self, feature: str) -> bool: """ Remove a single feature. :param feature: The namespace of the removed feature. """ if feature in self._features: self._features.remove(feature) for feature_xml in self.xml.findall('{%s}feature' % self.namespace): if feature_xml.attrib['var'] == feature: self.xml.remove(feature_xml) return True return False def get_features(self, dedupe: bool = True) -> Iterable[str]: """Return the set of all supported features.""" features: Union[List[str], Set[str]] if dedupe: features = set() else: features = [] for feature_xml in self.xml.findall('{%s}feature' % self.namespace): feature = feature_xml.attrib.get('var', None) if feature: if isinstance(features, set): features.add(feature) else: features.append(feature) return features def set_features(self, features: Iterable[str]): """ Add or replace the set of supported features. :param features: The new set of supported features. """ self.del_features() for feature in features: self.add_feature(feature) def del_features(self): """Remove all features.""" self._features = set() for feature_xml in self.xml.findall('{%s}feature' % self.namespace): self.xml.remove(feature_xml) slixmpp/slixmpp/plugins/xep_0030/stanza/items.py000066400000000000000000000110451477105560000221660ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import ( Iterable, Optional, Set, Tuple, ) from slixmpp import JID from slixmpp.xmlstream import ( ElementBase, ET, register_stanza_plugin, ) class DiscoItem(ElementBase): name = 'item' namespace = 'http://jabber.org/protocol/disco#items' plugin_attrib = name interfaces = {'jid', 'node', 'name'} def get_node(self) -> Optional[str]: """Return the item's node name or ``None``.""" return self._get_attr('node', None) def get_name(self) -> Optional[str]: """Return the item's human readable name, or ``None``.""" return self._get_attr('name', None) class DiscoItems(ElementBase): """ Example disco#items stanzas: .. code-block:: xml """ name = 'query' namespace = 'http://jabber.org/protocol/disco#items' plugin_attrib = 'disco_items' #: Stanza Interface: #: #: - ``node``: The name of the node to either #: query or return info from. #: - ``items``: A list of 3-tuples, where each tuple contains #: the JID, node, and name of an item. #: interfaces = {'node', 'items'} # Cache items _items: Set[Tuple[JID, Optional[str]]] def setup(self, xml: Optional[ET.ElementTree] = None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. :param xml: Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._items = {item[0:2] for item in self['items']} def add_item(self, jid: JID, node: Optional[str] = None, name: Optional[str] = None): """ Add a new item element. Each item is required to have a JID, but may also specify a node value to reference non-addressable entitities. :param jid: The JID for the item. :param node: Optional additional information to reference non-addressable items. :param name: Optional human readable name for the item. """ if (jid, node) not in self._items: self._items.add((jid, node)) item = DiscoItem(parent=self) item['jid'] = jid item['node'] = node item['name'] = name self.iterables.append(item) return True return False def del_item(self, jid: JID, node: Optional[str] = None) -> bool: """ Remove a single item. :param jid: JID of the item to remove. :param node: Optional extra identifying information. """ if (jid, node) in self._items: for item_xml in self.xml.findall('{%s}item' % self.namespace): item = (item_xml.attrib['jid'], item_xml.attrib.get('node', None)) if item == (jid, node): self.xml.remove(item_xml) return True return False def get_items(self) -> Set[DiscoItem]: """Return all items.""" items = set() for item in self['substanzas']: if isinstance(item, DiscoItem): items.add((item['jid'], item['node'], item['name'])) return items def set_items(self, items: Iterable[DiscoItem]): """ Set or replace all items. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name) :param items: A series of items in tuple format. """ self.del_items() for item in items: jid, node, name = item self.add_item(jid, node, name) def del_items(self): """Remove all items.""" self._items = set() items = [i for i in self.iterables if isinstance(i, DiscoItem)] for item in items: self.xml.remove(item.xml) self.iterables.remove(item) register_stanza_plugin(DiscoItems, DiscoItem, iterable=True) slixmpp/slixmpp/plugins/xep_0030/static.py000066400000000000000000000414511477105560000210400ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import annotations import logging from typing import ( Optional, Any, Dict, Tuple, TYPE_CHECKING, Union, Collection, ) from slixmpp import BaseXMPP, JID from slixmpp.stanza import Iq from slixmpp.types import TypedDict, OptJidStr, OptJid from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.plugins.xep_0030 import DiscoInfo, DiscoItems log = logging.getLogger(__name__) if TYPE_CHECKING: from slixmpp.plugins.xep_0030 import XEP_0030 class NodeType(TypedDict): info: DiscoInfo items: DiscoItems NodesType = Dict[ Tuple[str, str, str], NodeType ] class StaticDisco: """ While components will likely require fully dynamic handling of service discovery information, most clients and simple bots only need to manage a few disco nodes that will remain mostly static. StaticDisco provides a set of node handlers that will store static sets of disco info and items in memory. :var nodes: A dictionary mapping (JID, node) tuples to a dict containing a disco#info and a disco#items stanza. :var xmpp: The main Slixmpp object. :var disco: The instance of the XEP-0030 plugin. """ def __init__(self, xmpp: 'BaseXMPP', disco: 'XEP_0030'): """ Create a static disco interface. Sets of disco#info and disco#items are maintained for every given JID and node combination. These stanzas are used to store disco information in memory without any additional processing. :param xmpp: The main Slixmpp object. :param disco: The XEP-0030 plugin. """ self.nodes: NodesType = {} self.xmpp: BaseXMPP = xmpp self.disco: 'XEP_0030' = disco def add_node(self, jid: OptJidStr = None, node: Optional[str] = None, ifrom: OptJidStr = None) -> NodeType: if jid is None: node_jid = self.xmpp.boundjid.full elif isinstance(jid, JID): node_jid = jid.full else: node_jid = jid if ifrom is None: node_ifrom = '' elif isinstance(ifrom, JID): node_ifrom = ifrom.full else: node_ifrom = ifrom if node is None: node = '' if (node_jid, node, node_ifrom) not in self.nodes: node_dict: NodeType = { 'info': DiscoInfo(), 'items': DiscoItems(), } node_dict['info']['node'] = node node_dict['items']['node'] = node self.nodes[(node_jid, node, node_ifrom)] = node_dict return self.nodes[(node_jid, node, node_ifrom)] def get_node(self, jid: OptJidStr = None, node: Optional[str] = None, ifrom: OptJidStr = None) -> NodeType: if jid is None: node_jid = self.xmpp.boundjid.full elif isinstance(jid, JID): node_jid = jid.full else: node_jid = jid if node is None: node = '' if ifrom is None: node_ifrom = '' elif isinstance(ifrom, JID): node_ifrom = ifrom.full else: node_ifrom = ifrom if (node_jid, node, node_ifrom) not in self.nodes: self.add_node(node_jid, node, node_ifrom) return self.nodes[(node_jid, node, node_ifrom)] def node_exists(self, jid: OptJidStr = None, node: Optional[str] = None, ifrom: OptJidStr = None) -> bool: if jid is None: node_jid = self.xmpp.boundjid.full elif isinstance(jid, JID): node_jid = jid.full else: node_jid = jid if node is None: node = '' if ifrom is None: node_ifrom = '' elif isinstance(ifrom, JID): node_ifrom = ifrom.full else: node_ifrom = ifrom return (node_jid, node, node_ifrom) in self.nodes # ================================================================= # Node Handlers # # Each handler accepts four arguments: jid, node, ifrom, and data. # The jid and node parameters together determine the set of info # and items stanzas that will be retrieved or added. Additionally, # the ifrom value allows for cached results when results vary based # on the requester's JID. The data parameter is a dictionary with # additional parameters that will be passed to other calls. # # This implementation does not allow different responses based on # the requester's JID, except for cached results. To do that, # register a custom node handler. async def supports(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any) -> Optional[bool]: """ Check if a JID supports a given feature. The data parameter may provide: :param feature: The feature to check for support. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. :param cached: If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ feature = data.get('feature', None) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} if not feature: return False try: info = await self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) features = info['disco_info']['features'] return feature in features except IqError: return False except IqTimeout: return None async def has_identity(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Any] ) -> Optional[bool]: """ Check if a JID has a given identity. The data parameter may provide: :param category: The category of the identity to check. :param itype: The type of the identity to check. :param lang: The language of the identity to check. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. :param cached: If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ identity = (data.get('category', None), data.get('itype', None), data.get('lang', None)) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} try: info = await self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) def trunc(i): return (i[0], i[1], i[2]) return identity in map(trunc, info['disco_info']['identities']) except IqError: return False except IqTimeout: return None def get_info(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any) -> Optional[DiscoInfo]: """ Return the stored info data for the requested JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): if not node: return DiscoInfo() else: raise XMPPError(condition='item-not-found') else: return self.get_node(jid, node)['info'] def set_info(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: DiscoInfo): """ Set the entire info stanza for a JID/node at once. The data parameter is a disco#info substanza. """ new_node = self.add_node(jid, node) new_node['info'] = data def del_info(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any): """ Reset the info stanza for a given JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'] = DiscoInfo() def get_items(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any) -> Optional[DiscoItems]: """ Return the stored items data for the requested JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): if not node: return DiscoItems() else: raise XMPPError(condition='item-not-found') else: return self.get_node(jid, node)['items'] def set_items(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Collection[Tuple]]): """ Replace the stored items data for a JID/node combination. The data parameter may provide: items -- A set of items in tuple format. """ items = data.get('items', set()) new_node = self.add_node(jid, node) new_node['items']['items'] = items def del_items(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any): """ Reset the items stanza for a given JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): self.get_node(jid, node)['items'] = DiscoItems() def add_identity(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Optional[str]]): """ Add a new identity to the JID/node combination. The data parameter may provide: :param category: The general category to which the agent belongs. :param itype: A more specific designation with the category. :param name: Optional human readable name for this identity. :param lang: Optional standard xml:lang value. """ new_node = self.add_node(jid, node) new_node['info'].add_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def set_identities(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Collection[str]]): """ Add or replace all identities for a JID/node combination. The data parameter should include: :param identities: A list of identities in tuple form: (category, type, name, lang) """ identities = data.get('identities', set()) new_node = self.add_node(jid, node) new_node['info']['identities'] = identities def del_identity(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Optional[str]]): """ Remove an identity from a JID/node combination. The data parameter may provide: :param category: The general category to which the agent belonged. :param itype: A more specific designation with the category. :param name: Optional human readable name for this identity. :param lang: Optional, standard xml:lang value. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'].del_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def del_identities(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any): """ Remove all identities from a JID/node combination. The data parameter is not used. """ if self.node_exists(jid, node): del self.get_node(jid, node)['info']['identities'] def add_feature(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, str]): """ Add a feature to a JID/node combination. The data parameter should include: :param feature: The namespace of the supported feature. """ new_node = self.add_node(jid, node) new_node['info'].add_feature( data.get('feature', '')) def set_features(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, Collection[str]]): """ Add or replace all features for a JID/node combination. The data parameter should include: :param features: The new set of supported features. """ features = data.get('features', set()) new_node = self.add_node(jid, node) new_node['info']['features'] = features def del_feature(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, str]): """ Remove a feature from a JID/node combination. The data parameter should include: :param feature: The namespace of the removed feature. """ if self.node_exists(jid, node): self.get_node(jid, node)['info'].del_feature( data.get('feature', '')) def del_features(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any): """ Remove all features from a JID/node combination. The data parameter is not used. """ if not self.node_exists(jid, node): return del self.get_node(jid, node)['info']['features'] def add_item(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, str]): """ Add an item to a JID/node combination. The data parameter may include: :param ijid: The JID for the item. :param inode: Optional additional information to reference non-addressable items. :param name: Optional human readable name for the item. """ new_node = self.add_node(jid, node) new_node['items'].add_item( data.get('ijid', ''), node=data.get('inode', ''), name=data.get('name', '')) def del_item(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Dict[str, str]): """ Remove an item from a JID/node combination. The data parameter may include: :param ijid: JID of the item to remove. :param inode: Optional extra identifying information. """ if self.node_exists(jid, node): self.get_node(jid, node)['items'].del_item( data.get('ijid', ''), node=data.get('inode', None)) def cache_info(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Union[Iq, DiscoInfo]): """ Cache disco information for an external JID. The data parameter is the Iq result stanza containing the disco info to cache, or the disco#info substanza itself. """ if isinstance(data, Iq): info = data['disco_info'] else: info = data new_node = self.add_node(jid, node, ifrom) new_node['info'] = info def get_cached_info(self, jid: OptJid, node: Optional[str], ifrom: OptJid, data: Any) -> Optional[DiscoInfo]: """ Retrieve cached disco info data. The data parameter is not used. """ if not self.node_exists(jid, node, ifrom): return None return self.get_node(jid, node, ifrom)['info'] slixmpp/slixmpp/plugins/xep_0033/000077500000000000000000000000001477105560000171755ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0033/__init__.py000066400000000000000000000006351477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0033 import stanza from slixmpp.plugins.xep_0033.stanza import Addresses, Address from slixmpp.plugins.xep_0033.addresses import XEP_0033 register_plugin(XEP_0033) slixmpp/slixmpp/plugins/xep_0033/addresses.py000066400000000000000000000016301477105560000215240ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0033 import stanza, Addresses class XEP_0033(BasePlugin): """ XEP-0033: Extended Stanza Addressing """ name = 'xep_0033' description = 'XEP-0033: Extended Stanza Addressing' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, Addresses) register_stanza_plugin(Presence, Addresses) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Addresses.namespace) slixmpp/slixmpp/plugins/xep_0033/stanza.py000066400000000000000000000071041477105560000210510ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import JID, ElementBase, ET, register_stanza_plugin class Addresses(ElementBase): name = 'addresses' namespace = 'http://jabber.org/protocol/address' plugin_attrib = 'addresses' interfaces = set() def add_address(self, atype='to', jid='', node='', uri='', desc='', delivered=False): addr = Address(parent=self) addr['type'] = atype addr['jid'] = jid addr['node'] = node addr['uri'] = uri addr['desc'] = desc addr['delivered'] = delivered return addr # Additional methods for manipulating sets of addresses # based on type are generated below. class Address(ElementBase): name = 'address' namespace = 'http://jabber.org/protocol/address' plugin_attrib = 'address' interfaces = {'type', 'jid', 'node', 'uri', 'desc', 'delivered'} address_types = {'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'} def get_jid(self): return JID(self._get_attr('jid')) def set_jid(self, value): self._set_attr('jid', str(value)) def get_delivered(self): value = self._get_attr('delivered', False) return value and value.lower() in ('true', '1') def set_delivered(self, delivered): if delivered: self._set_attr('delivered', 'true') else: del self['delivered'] def set_uri(self, uri): if uri: del self['jid'] del self['node'] self._set_attr('uri', uri) else: self._del_attr('uri') # ===================================================================== # Auto-generate address type filters for the Addresses class. def _addr_filter(atype): def _type_filter(addr): if isinstance(addr, Address): if atype == 'all' or addr['type'] == atype: return True return False return _type_filter def _build_methods(atype): def get_multi(self): return list(filter(_addr_filter(atype), self)) def set_multi(self, value): del self[atype] for addr in value: # Support assigning dictionary versions of addresses # instead of full Address objects. if not isinstance(addr, Address): if atype != 'all': addr['type'] = atype elif 'atype' in addr and 'type' not in addr: addr['type'] = addr['atype'] addrObj = Address() addrObj.values = addr addr = addrObj self.append(addr) def del_multi(self): res = list(filter(_addr_filter(atype), self)) for addr in res: self.iterables.remove(addr) self.xml.remove(addr.xml) return get_multi, set_multi, del_multi for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'): get_multi, set_multi, del_multi = _build_methods(atype) Addresses.interfaces.add(atype) setattr(Addresses, "get_%s" % atype, get_multi) setattr(Addresses, "set_%s" % atype, set_multi) setattr(Addresses, "del_%s" % atype, del_multi) if atype == 'all': Addresses.interfaces.add('addresses') setattr(Addresses, "get_addresses", get_multi) setattr(Addresses, "set_addresses", set_multi) setattr(Addresses, "del_addresses", del_multi) register_stanza_plugin(Addresses, Address, iterable=True) slixmpp/slixmpp/plugins/xep_0045/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0045/__init__.py000066400000000000000000000006321477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2020 "Maxime “pep” Buquet " # 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) slixmpp/slixmpp/plugins/xep_0045/muc.py000066400000000000000000001027751477105560000203520ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # Copyright (C) 2020 "Maxime “pep” Buquet " # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import with_statement import asyncio import logging from collections import defaultdict from datetime import datetime from typing import ( Any, Dict, List, Tuple, Optional, ) from slixmpp import ( Presence, Message, Iq, JID, ) from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.matcher.stanzapath import StanzaPath from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask from slixmpp.exceptions import PresenceError from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0045 import stanza from slixmpp.plugins.xep_0045.stanza import ( MUCInvite, MUCDecline, MUCDestroy, MUCPresence, MUCJoin, MUCMessage, MUCAdminQuery, MUCAdminItem, MUCHistory, MUCOwnerQuery, MUCOwnerDestroy, MUCStatus, MUCActor, MUCUserItem, ) from slixmpp.types import ( JidStr, MucRole, MucAffiliation, MucRoomItem, MucRoomItemKeys, PresenceArgs, PresenceShows, ) JoinResult = Tuple[Presence, Message, List[Presence], List[Message]] log = logging.getLogger(__name__) AFFILIATIONS = ('outcast', 'member', 'admin', 'owner', 'none') ROLES = ('moderator', 'participant', 'visitor', 'none') class XEP_0045(BasePlugin): """ XEP-0045 Multi-User Chat. This XEP is made for use by *clients* or components acting as clients. If a component is made to act as MUC service, it should only care about the events and the stanza plugins. A single component may want to simulate joins as multiple entities, in which case it should enable the multi_from config option. """ name = 'xep_0045' description = 'XEP-0045: Multi-User Chat' dependencies = {'xep_0004', 'xep_0030', 'xep_0172', 'xep_0203'} stanza = stanza default_config = { 'multi_from': False, } rooms: Dict[Optional[JID], Dict[JID, Dict[str, MucRoomItem]]] our_nicks: Dict[Optional[JID], Dict[JID, str]] def plugin_init(self): self.rooms = defaultdict(lambda: defaultdict()) self.our_nicks = defaultdict(lambda: defaultdict()) # load MUC support in presence stanzas register_stanza_plugin(MUCMessage, MUCUserItem) register_stanza_plugin(MUCPresence, MUCUserItem) register_stanza_plugin(MUCUserItem, MUCActor) register_stanza_plugin(MUCMessage, MUCInvite) register_stanza_plugin(MUCMessage, MUCDecline) register_stanza_plugin(MUCMessage, MUCStatus) register_stanza_plugin(MUCPresence, MUCStatus) register_stanza_plugin(Presence, MUCPresence) register_stanza_plugin(MUCPresence, MUCDestroy) 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(MUCOwnerQuery, Form) register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True) # Register handlers self.xmpp.register_handler( Callback( 'MUCPresence', StanzaPath("presence/muc"), self._handle_groupchat_presence, )) # is only used in # presence when joining on the client side, and for errors on # the server side. if self.xmpp.is_component: self.xmpp.register_handler( Callback( 'MUCPresenceJoin', StanzaPath("presence/muc_join"), self._handle_groupchat_join, )) self.xmpp.register_handler( Callback( "MUCPresenceError", StanzaPath("presence@type=error/muc_join"), self._handle_presence_error, ) ) self.xmpp.register_handler( Callback( 'MUCError', MatchXMLMask("" % self.xmpp.default_ns), self._handle_groupchat_error_message )) self.xmpp.register_handler( Callback( 'MUCMessage', MatchXMLMask("" % self.xmpp.default_ns), self._handle_groupchat_message )) self.xmpp.register_handler( Callback( 'MUCSubject', MatchXMLMask("" % self.xmpp.default_ns), self._handle_groupchat_subject )) self.xmpp.register_handler( Callback( 'MUCConfig', StanzaPath('message/muc/status'), self._handle_config_change )) self.xmpp.register_handler( Callback( 'MUCInvite', StanzaPath('message/muc/invite'), self._handle_groupchat_invite )) self.xmpp.register_handler( Callback( 'MUCDecline', StanzaPath('message/muc/decline'), self._handle_groupchat_decline )) if not self.xmpp.is_component: self.multi_from = False 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: Message): """ Handle an invite into a muc. """ if self.xmpp.is_component: self.xmpp.event('groupchat_invite', inv) else: if inv['from'] not in self.rooms[None].keys(): self.xmpp.event("groupchat_invite", inv) def _handle_groupchat_decline(self, decl: Message): """Handle an invitation decline.""" if self.xmpp.is_component: self.xmpp.event('groupchat_invite', decl) else: if decl['from'] in self.room.keys(): self.xmpp.event('groupchat_decline', decl) def _handle_config_change(self, msg: Message): """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_presence(self, pr: Presence): """As a client, handle a presence stanza""" got_offline = False got_online = False if self.multi_from: rooms = self.rooms[pr['to']] else: rooms = self.rooms[None] if pr['muc']['room'] not in 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'] alt_nick_element = pr.get_plugin('nick', check=True) entry['alt_nick'] = alt_nick_element['nick'] if alt_nick_element else '' if pr['type'] == 'unavailable': if entry['nick'] in rooms[entry['room']]: del rooms[entry['room']][entry['nick']] got_offline = True else: if entry['nick'] not in rooms[entry['room']]: got_online = True rooms[entry['room']][entry['nick']] = entry log.debug("MUC presence from %s/%s : %s", entry['room'], entry['nick'], entry) status_codes = pr['muc']['status_codes'] if 110 in status_codes: if 303 in status_codes: if self.multi_from: pto = pr['to'] else: pto = None self.our_nicks[pto][pr['from'].bare] = pr['muc']['item_nick'] self.xmpp.event("muc::%s::self-presence" % entry['room'], 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_presence_error(self, pr: Presence): """Generate MUC presence error events""" self.xmpp.event("muc::%s::presence-error" % pr['from'].bare, pr) def _handle_groupchat_presence(self, pr: Presence): """ Handle a presence in a muc.""" self.xmpp.event("groupchat_presence", pr) self._handle_presence(pr) def _handle_groupchat_join(self, pr: Presence): """Received a join presence (as a component)""" self.xmpp.event('groupchat_join', pr) def _handle_groupchat_message(self, msg: Message): """ 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: Message): """ 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): """ 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 self.xmpp.event('groupchat_subject', msg) self.xmpp.event('muc::%s::groupchat_subject' % msg['from'].bare, msg) def make_join_stanza(self, room: JID, nick: str, *, password: Optional[str] = None, maxchars: Optional[int] = None, maxstanzas: Optional[int] = None, seconds: Optional[int] = None, since: Optional[datetime] = None, presence_options: Optional[PresenceArgs] = None) -> Presence: """ Build the stanza for the MUC join, without sending it. Only one of {maxchars, maxstanzas, seconds, since} will be used, in that order. .. versionadded:: 1.9.0 :param room: Room JID :param nick: User nickname in the room :param password: The optional room password. :param maxchars: Max number of characters to return from history. :param maxstanzas: Max number of stanzas to return from history. :param seconds: Fetch history until that many seconds in the past. :param since: Fetch history since that timestamp. :return: The presence stanza to send """ if presence_options is None: presence_options = {} elif presence_options.get('type') == 'unavailable': del presence_options['type'] pto = JID(room) pto.resource = nick stanza = self.xmpp.make_presence( pto=pto, **presence_options ) stanza.enable('muc_join') if password is not None: stanza['muc_join']['password'] = password if maxchars is not None: stanza['muc_join']['history']['maxchars'] = str(maxchars) elif maxstanzas is not None: stanza['muc_join']['history']['maxstanzas'] = str(maxstanzas) elif seconds is not None: stanza['muc_join']['history']['seconds'] = str(seconds) elif since is not None: fmt = self.xmpp.plugin['xep_0082'].format_datetime(since) stanza['muc_join']['history']['since'] = fmt return stanza async def join_muc_wait(self, room: JID, nick: str, *, password: Optional[str] = None, maxchars: Optional[int] = None, maxstanzas: Optional[int] = None, seconds: Optional[int] = None, since: Optional[datetime] = None, presence_options: Optional[PresenceArgs] = None, timeout: int = 300) -> JoinResult: """ Try to join a MUC and block until we are joined or get an error. Only one of {maxchars, maxstanzas, seconds, since} will be used, in that order. .. versionadded:: 1.8.0 :param room: Room JID :param nick: User nickname in the room :param password: The optional room password. :param maxchars: Max number of characters to return from history. :param maxstanzas: Max number of stanzas to return from history. :param seconds: Fetch history until that many seconds in the past. :param since: Fetch history since that timestamp. :param timeout: Timeout after which a TimeoutError is raised. None means no timeout. :raises: A slixmpp.exceptions.PresenceError if the MUC returns a presence error. :raises: An asyncio.TimeoutError if there is neither success nor presence error when the timeout is reached. :return: A tuple containing our own presence, the subject, a list of occupants and a list of history messages. """ if self.xmpp.is_component and not presence_options.get('pfrom'): raise ValueError('Components must always set the pfrom= attribute.') stanza = self.make_join_stanza( room=room, nick=nick, password=password, maxchars=maxchars, maxstanzas=maxstanzas, seconds=seconds, since=since, presence_options=presence_options, ) if self.multi_from: pfrom = presence_options['pfrom'] else: pfrom = None self.rooms[pfrom][room] = {} self.our_nicks[pfrom][room] = nick return await self._await_join(room, stanza, timeout) async def _await_join(self, room: JID, stanza: Presence, timeout: int = 300) -> JoinResult: """Do the heavy lifting for awaiting a MUC join A muc join, once the join stanza is sent, is: occupant presences → self-presence → room history → room subject """ presence_done: asyncio.Future = asyncio.Future() topic_received: asyncio.Future = asyncio.Future() history_buffer: List[Message] = [] occupant_buffer: List[Presence] = [] pfrom = stanza['from'] or None def add_message(msg: Message): if pfrom and pfrom != msg['to']: return delay = msg.get_plugin('delay', check=True) if delay is not None and delay['from'] == room: history_buffer.append(msg) def add_occupant(pres: Presence): if pfrom and pfrom != pres['to']: return occupant_buffer.append(pres) def set_topic(msg: Message): if pfrom and pfrom != msg['to']: return if not topic_received.done(): topic_received.set_result(msg) def set_self_presence(pres: Presence): if pfrom and pfrom != pres['to']: return if not presence_done.done(): presence_done.set_result(pres) catch_occupants = self.xmpp.event_handler( "muc::%s::got_online" % room, add_occupant ) catch_history = self.xmpp.event_handler( "muc::%s::message" % room, add_message ) subject_handler = self.xmpp.event_handler( "muc::%s::groupchat_subject" % room, set_topic ) self_presence = self.xmpp.event_handler( "muc::%s::self-presence" % room, set_self_presence ) presence_error = self.xmpp.event_handler( "muc::%s::presence-error" % room, set_self_presence ) with subject_handler, catch_history, catch_occupants: with self_presence, presence_error: stanza.send() done, pending = await asyncio.wait( [presence_done], timeout=timeout, ) if pending: raise asyncio.TimeoutError() pres: Presence = presence_done.result() if pres['type'] == 'error': raise PresenceError(pres) done, pending = await asyncio.wait( [topic_received], timeout=timeout, ) if pending: raise asyncio.TimeoutError() subject: Message = topic_received.result() # update known nick in case it has changed self.our_nicks[pfrom][room] = pres['from'].resource return (pres, subject, occupant_buffer, history_buffer) def join_muc(self, room: JID, nick: str, maxhistory: str = "0", password: str = '', pstatus: str = '', pshow: PresenceShows = 'chat', pfrom: JidStr = '' ) -> asyncio.Future: """ Join the specified room, requesting 'maxhistory' lines of history. .. deprecated:: 1.8.0 :meth:`join_muc_wait` will replace this old API starting from version 1.9.0. """ presence_options = PresenceArgs( pshow=pshow, pstatus=pstatus, pfrom=pfrom, ) maxchars, maxstanzas = None, None if maxhistory: if maxhistory == "0": maxchars = 9 else: maxstanzas = int(maxhistory) return asyncio.ensure_future( self.join_muc_wait( room=room, nick=nick, password=password, presence_options=presence_options, maxchars=maxchars, maxstanzas=maxstanzas, ), loop=self.xmpp.loop, ) def leave_muc(self, room: JID, nick: str, msg: str = '', pfrom: Optional[JID] = None): """ Leave the specified room. :param room: Room to leave. :param nick: Your nickname. :param msg: Presence status to use. :raises: KeyError if the room is not in our room list. """ 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 ) if pfrom in self.rooms and room in self.rooms[pfrom]: del self.rooms[pfrom][room] else: raise KeyError( f'Unable to find the room {room} in the currently joined rooms' + (f' for {pfrom}' if pfrom else '') ) def set_subject(self, room: JidStr, subject: str, *, mfrom: Optional[JID] = None): """Set a room’s subject. :param room: JID of the room. :param subject: Room subject to set. """ msg = self.xmpp.make_message(room, mfrom=mfrom) msg['type'] = 'groupchat' msg['subject'] = subject msg.send() async def get_room_config(self, room: JidStr, ifrom: Optional[JID] = None, **iqkwargs) -> Form: """Get the room config form in 0004 plugin format. :param room: Room to get the config form from. :raises ValueError: When the form is not found. :returns: A form object. """ iq = self.xmpp.make_iq_get(stanza.NS_OWNER, ito=room, ifrom=ifrom) result = await iq.send(**iqkwargs) form = result['mucowner_query'].get_plugin('form', check=True) if form is None: raise ValueError("Configuration form not found") return form async def set_room_config(self, room: JidStr, config: Form, *, ifrom: Optional[JID] = None, **iqkwargs): """Send a room config form. :param room: Room to send the form to. :param config: A filled room form. """ query = MUCOwnerQuery() config['type'] = 'submit' query.append(config) iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom) await iq.send(**iqkwargs) async def cancel_config(self, room: JidStr, *, ifrom: Optional[JidStr] = None, **iqkwargs): """Cancel a requested config form. :param room: Room to cancel the form for. """ query = MUCOwnerQuery() query['form']['type'] = 'cancel' iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom) await iq.send(**iqkwargs) async def destroy(self, room: JidStr, reason: str = '', altroom: Optional[JidStr] = None, *, ifrom: Optional[JidStr] = None, **iqkwargs): """Destroy a room. :param room: Room JID to destroy. :param reason: Reason for destroying the room. :param altroom: An alternate room that users should join. """ 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: JidStr, affiliation: MucAffiliation, *, jid: Optional[JidStr] = None, nick: Optional[str] = None, reason: str = '', ifrom: Optional[JidStr] = None, **iqkwargs): """ Change room affiliation for a JID or nickname. :param room: Room to modify. :param affiliation: Affiliation to set. :param jid: User JID to use in the set operation. :param reason: Reason for the affiliation change. """ if affiliation not in AFFILIATIONS: raise ValueError('%s is not a valid affiliation' % affiliation) if affiliation == 'outcast' and not jid: raise ValueError('Outcast affiliation requires using a jid') 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['mucadmin_query']['item']['affiliation'] = affiliation if nick: iq['mucadmin_query']['item']['nick'] = nick if jid: iq['mucadmin_query']['item']['jid'] = jid if reason: iq['mucadmin_query']['item']['reason'] = reason await iq.send(**iqkwargs) async def get_affiliation_list(self, room: JidStr, affiliation: MucAffiliation, *, ifrom: Optional[JidStr] = None, **iqkwargs) -> List[JID]: """Get a list of JIDs with the specified affiliation :param room: Room to get affiliations from. :param affiliation: The affiliation to list. """ 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 send_affiliation_list(self, room: JidStr, affiliations: List[Tuple[JidStr, MucAffiliation]], *, ifrom: Optional[JidStr] = None, **iqkwargs): """Send an affiliation delta list. :param room: Room to send the affiliations to. :param affiliations: List of couples (jid, affiliation) to set. """ 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) await iq.send(**iqkwargs) async def set_role(self, room: JidStr, nick: str, role: MucRole, *, reason: str = '', ifrom: Optional[JidStr] = None, **iqkwargs): """ 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). :param room: Room to modify. :param nick: User nickname to use in the set operation. :param role: Role to set. :param reason: Reason for the role change. """ if role not in ROLES: raise ValueError("Role %s does not exist" % role) iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom) iq['mucadmin_query']['item']['role'] = role iq['mucadmin_query']['item']['nick'] = nick if reason: iq['mucadmin_query']['item']['reason'] = reason await iq.send(**iqkwargs) async def get_roles_list(self, room: JidStr, role: MucRole, *, ifrom: Optional[JidStr] = None, **iqkwargs) -> List[str]: """"Get a list of JIDs with the specified role :param room: Room to get roles from. :param role: The role to list. """ 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_role_list(self, room: JidStr, roles: List[Tuple[str, MucRole]], *, ifrom: Optional[JidStr] = None, **iqkwargs): """Send a role delta list. :param room: Room to send the roles to. :param roles: List of couples (nick, role) to set. """ 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) await iq.send(**iqkwargs) def invite(self, room: JidStr, jid: JidStr, reason: str = '', *, mfrom: Optional[JidStr] = None): """ Invite a jid to a room (mediated invitation). :param room: Room to invite the user in. :param jid: JID of the user to invite. :param reason: Reason for inviting the user. """ msg = self.xmpp.make_message(room, mfrom=mfrom) msg['muc']['invite']['to'] = jid if reason: msg['muc']['invite']['reason'] = reason self.xmpp.send(msg) def invite_server(self, room: JidStr, jid: JidStr, invite_from: JidStr, reason: str = ''): """Send a mediated invite to a user, as a MUC service. .. versionadded:: 1.8.0 :param room: Room to invite the user in. :param jid: JID of the user to invite. :param invite_from: JID of the user to send the invitation from. :param reason: Reason for inviting the user. """ if not self.xmpp.is_component: raise ValueError("Cannot use this method as a client.") msg = self.xmpp.make_message(jid, mfrom=room) msg['muc']['invite']['from'] = invite_from if reason: msg['muc']['invite']['reason'] = reason msg.send() def decline(self, room: JidStr, jid: JidStr, reason: str = '', *, mfrom: Optional[JidStr] = None): """Decline a mediated invitation. :param room: Room the invitation came from. :param jid: JID of the user who sent the invitation. :param reason: Reason for declining. """ msg = self.xmpp.make_message(room, mfrom=mfrom) msg['muc']['decline']['to'] = jid if reason: msg['muc']['decline']['reason'] = reason self.xmpp.send(msg) def request_voice(self, room: JidStr, role: str, *, mfrom: Optional[JidStr] = None): """Request voice in a moderated room. :param room: Room to request voice from. """ msg = self.xmpp.make_message(room, mfrom=mfrom) form = msg['form'] form['type'] = 'submit' form.add_field( var='FORM_TYPE', ftype='hidden', value='http://jabber.org/protocol/muc#request', ) form.add_field(var='muc#role', ftype='list-single', label='Requested role', value=role) self.xmpp.send(msg) async def set_self_nick(self, room: JID, new_nick: str, timeout: int = 60, presence_options: Optional[PresenceArgs] = None) -> str: """ Set your nickname in a room. The room can arbitrarily decide on another nickname, so this function waits for the response and returns the final nickname. .. versionadded:: 1.9.0 :param room: Room in which to change our nickname :param new_nick: Our new nickname in the room :param timeout: Time to wait for our new nickname response :param pfrom: (for components) the JID to send our presence from """ new_jid = JID(room) new_jid.resource = new_nick if presence_options is None: presence_options = {} future = asyncio.Future() def nickname_set(presence): codes = presence['muc']['status_codes'] if 110 in codes and 303 in codes: future.set_result(presence) handler = self.xmpp.event_handler( f"muc::{room}::self-presence", nickname_set, ) with handler: self.xmpp.make_presence(pto=new_jid, **presence_options).send() done, pending = await asyncio.wait([future], timeout=timeout) if pending: raise TimeoutError("Timed out waiting for server answer") presence = future.result() new_nick = presence['muc']['item_nick'] return new_nick def jid_in_room(self, room: JID, jid: JID, pfrom: Optional[JID] = None) -> bool: """Check if a JID is present in a room. :param room: Room to check. :param jid: full JID to check. """ bare_match = False rooms = self.rooms.get(pfrom, {}) room = rooms.get(room, {}) for nick in room: entry = room[nick] if not entry.get('jid'): continue if entry['jid'] == jid.full: return True elif JID(entry['jid']).bare == jid.bare: bare_match = True if bare_match: logging.info( "Could not retrieve full JID, falling back to bare JID for %s in %s", jid, room ) return bare_match def get_nick(self, room: JID, jid: JID, pfrom: Optional[JID] = None) -> Optional[str]: """Get the nickname of a specific JID in a room. :param room: Room to inspect. :param jid: FULL JID whose nick to return. """ bare_match = None rooms = self.rooms.get(pfrom, {}) room = rooms.get(room, {}) for nick in room: entry = room[nick] if not entry.get('jid'): continue if entry['jid'] == jid.full: return nick elif JID(entry['jid']).bare == jid.bare: bare_match = nick if bare_match: logging.info( "Could not retrieve full JID, falling back to bare JID for %s in %s", jid, room ) return bare_match def get_joined_rooms(self, pfrom: Optional[JID] = None) -> List[JID]: """Get the list of rooms we sent a join presence to and did not explicitly leave. """ return list(self.rooms.get(pfrom, {}).keys()) def get_our_jid_in_room(self, room_jid: JID, pfrom: Optional[JID] = None) -> str: """ Return the jid we're using in a room. """ return "%s/%s" % (room_jid, self.our_nicks[pfrom][room_jid]) def get_jid_property(self, room: JID, nick: str, jid_property: MucRoomItemKeys, pfrom: Optional[JID] = None) -> Any: """ Get the property of a nick in a room, such as its 'jid' or 'affiliation' If not found, return None. :param room: Get the property for this room. :param nick: Which nickname information to get. :param jid_property: Property to fetch. """ rooms = self.rooms.get(pfrom, {}) room_dict = rooms.get(room, {}) nick_dict = room_dict.get(nick, {}) prop = nick_dict.get(jid_property) return prop or None def get_roster(self, room: JID, pfrom: Optional[JID] = None) -> List[str]: """ Get the list of nicks in a room. :param room: Room to list nicks from. """ rooms = self.rooms.get(pfrom, {}) if room not in rooms: raise ValueError("Room %s is not joined" % room) return list(rooms[room].keys()) def get_users_by_affiliation(self, room: JidStr, affiliation='member', *, ifrom: Optional[JidStr] = None): # Preserve old API if affiliation not in AFFILIATIONS: raise ValueError("Affiliation %s does not exist" % affiliation) return self.get_affiliation_list(room, affiliation, ifrom=ifrom) # Aliases muc→groupchat join_groupchat = join_muc join_groupchat_wait = join_muc_wait slixmpp/slixmpp/plugins/xep_0045/stanza.py000066400000000000000000000172311477105560000210560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # Copyright (C) 2020 "Maxime “pep” Buquet " # This file is part of Slixmpp. # See the file LICENSE for copying permission. from typing import ( Iterable, Set, Optional, Union, ) 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', 'status_codes', 'item_nick', } def get_status_codes(self) -> Set[int]: status = self.xml.findall(f'{{{NS_USER}}}status') return {int(status.attrib['code']) for status in status} def set_status_codes(self, codes: Iterable[int]): self.del_status_codes() for code in set(codes): self._add_status_code(code) def del_status_codes(self): status = self.xml.findall(f'{{{NS_USER}}}status') for elem in status: self.xml.remove(elem) def _add_status_code(self, code: int): status = MUCStatus() status['code'] = code self.append(status) def get_item_nick(self) -> str: return self.get_item_attr('nick', '') def set_item_nick(self, value: str) -> str: return self.set_item_attr('nick', value) def get_item_attr(self, attr: str, default): item = self.xml.find(f'{{{NS_USER}}}item') if item is None: return default return self['item'][attr] def set_item_attr(self, attr: str, value: str): item = self['item'] item[attr] = value return item def del_item_attr(self, attr): item = self.xml.find(f'{{{NS_USER}}}item') if item is not None: del self['item'][attr] def get_affiliation(self): return self.get_item_attr('affiliation', '') def set_affiliation(self, value): self.set_item_attr('affiliation', value) def del_affiliation(self): self.del_item_attr('affiliation') def get_jid(self) -> JID: return JID(self.get_item_attr('jid', '')) def set_jid(self, value: Union[JID, str]): if not isinstance(value, str): value = str(value) self.set_item_attr('jid', value) def del_jid(self): self.del_item_attr('jid') def get_role(self) -> str: return self.get_item_attr('role', '') def set_role(self, value: str): # TODO: check for valid role self.set_item_attr('role', value) def del_role(self): # TODO: set default role self.del_item_attr('role') def get_nick(self) -> str: return self.parent()['from'].resource def get_room(self) -> str: 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 :: ''' class MUCMessage(MUCBase): ''' A MUC Message :: Foo ''' 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', 'from', 'reason'} sub_interfaces = {'reason'} def get_to(self) -> JID: return JID(self._get_attr('to')) def set_to(self, value: Union[JID, str]): if not isinstance(value, JID): value = JID(value) self._set_attr('to', value) def get_from(self) -> JID: return JID(self._get_attr('from')) def set_from(self, value: Union[JID, str]): if not isinstance(value, JID): value = JID(value) self._set_attr('from', value) class MUCDecline(ElementBase): name = 'decline' plugin_attrib = 'decline' namespace = NS_USER interfaces = {'to', 'from', 'reason'} sub_interfaces = {'reason'} def get_to(self) -> JID: return JID(self._get_attr('to')) def set_to(self, value: Union[JID, str]): if not isinstance(value, JID): value = JID(value) self._set_attr('to', value) def get_from(self) -> JID: return JID(self._get_attr('from')) def set_from(self, value: Union[JID, str]): if not isinstance(value, JID): value = JID(value) self._set_attr('from', value) 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' namespace = NS_OWNER 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', 'reason'} sub_interfaces = {'reason'} class MUCStatus(ElementBase): namespace = NS_USER name = 'status' plugin_attrib = 'status' interfaces = {'code'} def set_code(self, code: int): self.xml.attrib['code'] = str(code) class MUCUserItem(ElementBase): namespace = NS_USER name = 'item' plugin_attrib = 'item' interfaces = {'role', 'affiliation', 'jid', 'reason', 'nick'} sub_interfaces = {'reason'} def get_jid(self) -> Optional[JID]: jid = self.xml.attrib.get('jid', None) if jid: return JID(jid) else: return None class MUCActor(ElementBase): namespace = NS_USER name = 'actor' plugin_attrib = 'actor' interfaces = {'jid', 'nick'} def get_jid(self) -> Optional[JID]: jid = self.xml.attrib.get('jid', None) if jid: return JID(jid) else: return None class MUCDestroy(ElementBase): name = 'destroy' plugin_attrib = 'destroy' namespace = NS_USER interfaces = {'reason', 'jid'} sub_interfaces = {'reason'} slixmpp/slixmpp/plugins/xep_0047/000077500000000000000000000000001477105560000172025ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0047/__init__.py000066400000000000000000000007171477105560000213200ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0047 import stanza from slixmpp.plugins.xep_0047.stanza import Open, Close, Data from slixmpp.plugins.xep_0047.stream import IBBytestream from slixmpp.plugins.xep_0047.ibb import XEP_0047 register_plugin(XEP_0047) slixmpp/slixmpp/plugins/xep_0047/ibb.py000066400000000000000000000177571477105560000203310ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # This file is part of Slixmpp # See the file LICENSE for copying permission import uuid import logging from typing import ( Optional, Union, ) from slixmpp import JID from slixmpp.stanza import Message, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream log = logging.getLogger(__name__) class XEP_0047(BasePlugin): """ XEP-0047: In-Band Bytestreams Events registered by this plugin: - :term:`ibb_stream_start` - :term:`ibb_stream_end` - :term:`ibb_stream_data` - :term:`stream:[stream id]:[peer jid]` Plugin Parameters: - ``block_size`` (default: ``4096``): default block size to negociate - ``max_block_size`` (default: ``8192``): max block size to accept - ``auto_accept`` (default: ``False``): if incoming streams should be accepted automatically. - :term:`authorized (0047 version)` - :term:`authorized_sid (0047 version)` - :term:`preauthorize_sid (0047 version)` - :term:`get_stream` - :term:`set_stream` - :term:`del_stream` """ name = 'xep_0047' description = 'XEP-0047: In-Band Bytestreams' dependencies = {'xep_0030'} stanza = stanza default_config = { 'block_size': 4096, 'max_block_size': 8192, 'auto_accept': False, } def plugin_init(self): self._streams = {} self._preauthed_sids = {} register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Data) register_stanza_plugin(Message, Data) self.xmpp.register_handler(CoroutineCallback( 'IBB Open', StanzaPath('iq@type=set/ibb_open'), self._handle_open_request)) self.xmpp.register_handler(CoroutineCallback( 'IBB Close', StanzaPath('iq@type=set/ibb_close'), self._handle_close)) self.xmpp.register_handler(CoroutineCallback( 'IBB Data', StanzaPath('iq@type=set/ibb_data'), self._handle_data)) self.xmpp.register_handler(CoroutineCallback( 'IBB Message Data', StanzaPath('message/ibb_data'), self._handle_data)) self.api.register(self._authorized, 'authorized', default=True) self.api.register(self._authorized_sid, 'authorized_sid', default=True) self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) self.api.register(self._get_stream, 'get_stream', default=True) self.api.register(self._set_stream, 'set_stream', default=True) self.api.register(self._del_stream, 'del_stream', default=True) def plugin_end(self): self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Close') self.xmpp.remove_handler('IBB Data') self.xmpp.remove_handler('IBB Message Data') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') def _get_stream(self, jid, sid, peer_jid, data): return self._streams.get((jid, sid, peer_jid), None) def _set_stream(self, jid, sid, peer_jid, stream): self._streams[(jid, sid, peer_jid)] = stream def _del_stream(self, jid, sid, peer_jid, data): if (jid, sid, peer_jid) in self._streams: del self._streams[(jid, sid, peer_jid)] async def _accept_stream(self, iq): receiver = iq['to'] sender = iq['from'] sid = iq['ibb_open']['sid'] if await self.api['authorized_sid'](receiver, sid, sender, iq): return True return await self.api['authorized'](receiver, sid, sender, iq) def _authorized(self, jid, sid, ifrom, iq): if self.auto_accept: return True return False def _authorized_sid(self, jid, sid, ifrom, iq): if (jid, sid, ifrom) in self._preauthed_sids: del self._preauthed_sids[(jid, sid, ifrom)] return True return False def _preauthorize_sid(self, jid, sid, ifrom, data): self._preauthed_sids[(jid, sid, ifrom)] = True async def open_stream(self, jid: JID, *, block_size: Optional[int] = None, sid: Optional[str] = None, use_messages: bool = False, ifrom: Optional[JID] = None, **iqkwargs) -> IBBytestream: """Open an IBB stream with a peer JID. .. versionchanged:: 1.8.0 This function is now a coroutine and must be awaited. All parameters except ``jid`` are keyword-args only. :param jid: The remote JID to initiate the stream with. :param block_size: The block size to advertise. :param sid: The IBB stream id (if not provided, will be auto-generated). :param use_messages: If the stream should use message stanzas instead of iqs. :returns: The opened byte stream with the remote JID :raises .IqError: When the remote entity denied the stream. """ if sid is None: sid = str(uuid.uuid4()) if block_size is None: block_size = self.block_size iq = self.xmpp.make_iq_set(ito=jid, ifrom=ifrom) iq['ibb_open']['block_size'] = block_size iq['ibb_open']['sid'] = sid iq['ibb_open']['stanza'] = 'message' if use_messages else 'iq' stream = IBBytestream(self.xmpp, sid, block_size, iq['from'], iq['to'], use_messages) callback = iqkwargs.pop('callback', None) result = await iq.send(**iqkwargs) log.debug('IBB stream (%s) accepted by %s', stream.sid, result['from']) stream.self_jid = result['to'] stream.peer_jid = result['from'] stream.stream_started = True await self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) if callback is not None: self.xmpp.add_event_handler('ibb_stream_start', callback, disposable=True) self.xmpp.event('ibb_stream_start', stream) self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream) return stream async def _handle_open_request(self, iq: Iq): sid = iq['ibb_open']['sid'] size = iq['ibb_open']['block_size'] or self.block_size log.debug('Received IBB stream request from %s', iq['from']) if not sid: raise XMPPError(etype='modify', condition='bad-request') if not await self._accept_stream(iq): raise XMPPError(etype='cancel', condition='not-acceptable') if size > self.max_block_size: raise XMPPError('resource-constraint') stream = IBBytestream(self.xmpp, sid, size, iq['to'], iq['from']) stream.stream_started = True await self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) iq.reply().send() self.xmpp.event('ibb_stream_start', stream) self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream) async def _handle_data(self, stanza: Union[Iq, Message]): sid = stanza['ibb_data']['sid'] stream = await self.api['get_stream'](stanza['to'], sid, stanza['from']) if stream is not None and stanza['from'] == stream.peer_jid: stream._recv_data(stanza) else: raise XMPPError('item-not-found') async def _handle_close(self, iq: Iq): sid = iq['ibb_close']['sid'] stream = await self.api['get_stream'](iq['to'], sid, iq['from']) if stream is not None and iq['from'] == stream.peer_jid: stream._closed(iq) await self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid) else: raise XMPPError('item-not-found') slixmpp/slixmpp/plugins/xep_0047/stanza.py000066400000000000000000000034331477105560000210570ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # This file is part of Slixmpp # See the file LICENSE for copying permission import re import base64 from slixmpp.util import bytes from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import ElementBase VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*') def to_b64(data): return bytes(base64.b64encode(bytes(data))).decode('utf-8') def from_b64(data): return bytes(base64.b64decode(bytes(data))) class Open(ElementBase): name = 'open' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_open' interfaces = {'block_size', 'sid', 'stanza'} def get_block_size(self): return int(self._get_attr('block-size', '0')) def set_block_size(self, value): self._set_attr('block-size', str(value)) def del_block_size(self): self._del_attr('block-size') class Data(ElementBase): name = 'data' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_data' interfaces = {'seq', 'sid', 'data'} sub_interfaces = {'data'} def get_seq(self): return int(self._get_attr('seq', '0')) def set_seq(self, value): self._set_attr('seq', str(value)) def get_data(self): text = self.xml.text if not text: raise XMPPError('not-acceptable', 'IBB data element is empty.') b64_data = text.strip() if VALID_B64.match(b64_data).group() == b64_data: return from_b64(b64_data) else: raise XMPPError('not-acceptable') def set_data(self, value): self.xml.text = to_b64(value) def del_data(self): self.xml.text = '' class Close(ElementBase): name = 'close' namespace = 'http://jabber.org/protocol/ibb' plugin_attrib = 'ibb_close' interfaces = {'sid'} slixmpp/slixmpp/plugins/xep_0047/stream.py000066400000000000000000000143001477105560000210450ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # This file is part of Slixmpp # See the file LICENSE for copying permission import asyncio import socket import logging from typing import ( Optional, IO, Union, ) from slixmpp import JID from slixmpp.stanza import Iq, Message from slixmpp.exceptions import XMPPError, IqTimeout log = logging.getLogger(__name__) class IBBytestream(object): """XEP-0047 Stream abstraction. Created by the ibb plugin automatically. Provides send methods and triggers :term:`ibb_stream_data` events. """ def __init__(self, xmpp, sid: str, block_size: int, jid: JID, peer: JID, use_messages: bool = False): self.xmpp = xmpp self.sid = sid self.block_size = block_size self.use_messages = use_messages if jid is None: jid = xmpp.boundjid self.self_jid = jid self.peer_jid = peer self.send_seq = -1 self.recv_seq = -1 self.stream_started = False self.stream_in_closed = False self.stream_out_closed = False self.recv_queue = asyncio.Queue() async def send(self, data: bytes, timeout: Optional[int] = None) -> int: """Send a single block of data. :param data: Data to send (will be truncated if above block size). :returns: Number of bytes sent. """ if not self.stream_started or self.stream_out_closed: raise socket.error if len(data) > self.block_size: data = data[:self.block_size] self.send_seq = (self.send_seq + 1) % 65536 seq = self.send_seq if self.use_messages: msg = self.xmpp.Message() msg['to'] = self.peer_jid msg['from'] = self.self_jid msg['id'] = self.xmpp.new_id() msg['ibb_data']['sid'] = self.sid msg['ibb_data']['seq'] = seq msg['ibb_data']['data'] = data msg.send() else: iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = self.peer_jid iq['from'] = self.self_jid iq['ibb_data']['sid'] = self.sid iq['ibb_data']['seq'] = seq iq['ibb_data']['data'] = data await iq.send(timeout=timeout) return len(data) async def sendall(self, data: bytes, timeout: Optional[int] = None): """Send all the contents of ``data`` in chunks. :param data: Raw data to send. """ sent_len = 0 while sent_len < len(data): sent_len += await self.send(data[sent_len:sent_len+self.block_size], timeout=timeout) async def gather(self, max_data: Optional[int] = None, timeout: int = 3600) -> bytes: """Gather all data sent on a stream until it is closed, and return it. .. versionadded:: 1.8.0 :param max_data: Max number of bytes to receive. (received data may be over this limit depending on block_size) :param timeout: Timeout after which an error will be raised. :raises .IqTimeout: If the timeout is reached. :returns: All bytes accumulated in the stream. """ result = b'' while not self.recv_queue.empty(): result += self.recv_queue.get_nowait() if max_data and len(result) > max_data: return result if self.stream_in_closed: return result end_future = asyncio.Future() def on_close(stream): if stream is self: end_future.set_result(True) def on_data(stream): nonlocal result if stream is self: result += stream.read() if max_data and len(result) > max_data: end_future.set_result(True) self.xmpp.add_event_handler('ibb_stream_end', on_close) self.xmpp.add_event_handler('ibb_stream_data', on_data) try: await asyncio.wait_for(end_future, timeout) except asyncio.TimeoutError: raise IqTimeout(result) finally: self.xmpp.del_event_handler('ibb_stream_end', on_close) self.xmpp.del_event_handler('ibb_stream_data', on_data) return result async def sendfile(self, file: IO[bytes], timeout: Optional[int] = None): """Send the contents of a file over the wire, in chunks. :param file: The opened file (or file-like) object, in bytes mode.""" while True: data = file.read(self.block_size) if not data: break await self.send(data, timeout=timeout) def _recv_data(self, stanza: Union[Message, Iq]): new_seq = stanza['ibb_data']['seq'] if new_seq != (self.recv_seq + 1) % 65536: self.close() raise XMPPError('unexpected-request') self.recv_seq = new_seq data = stanza['ibb_data']['data'] if len(data) > self.block_size: self.close() raise XMPPError('not-acceptable') self.recv_queue.put_nowait(data) self.xmpp.event('ibb_stream_data', self) if isinstance(stanza, Iq): stanza.reply().send() def recv(self, *args, **kwargs): return self.read() def read(self): if not self.stream_started or self.stream_in_closed: raise socket.error return self.recv_queue.get_nowait() def close(self, timeout: Optional[int] = None) -> asyncio.Future: """Close the stream.""" iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = self.peer_jid iq['from'] = self.self_jid iq['ibb_close']['sid'] = self.sid self.stream_out_closed = True def _close_stream(_): self.stream_in_closed = True future = iq.send(timeout=timeout, callback=_close_stream) self.xmpp.event('ibb_stream_end', self) return future def _closed(self, iq: Iq): self.stream_in_closed = True self.stream_out_closed = True iq.reply().send() self.xmpp.event('ibb_stream_end', self) def makefile(self, *args, **kwargs): return self def connect(self, *args, **kwargs): return None def shutdown(self, *args, **kwargs): return None slixmpp/slixmpp/plugins/xep_0048/000077500000000000000000000000001477105560000172035ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0048/__init__.py000066400000000000000000000005711477105560000213170ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0048.stanza import Bookmarks, Conference, URL from slixmpp.plugins.xep_0048.bookmarks import XEP_0048 register_plugin(XEP_0048) slixmpp/slixmpp/plugins/xep_0048/bookmarks.py000066400000000000000000000044441477105560000215530ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0048 import stanza, Bookmarks, Conference, URL log = logging.getLogger(__name__) class XEP_0048(BasePlugin): name = 'xep_0048' description = 'XEP-0048: Bookmarks' dependencies = {'xep_0045', 'xep_0049', 'xep_0060', 'xep_0163', 'xep_0223'} stanza = stanza default_config = { 'auto_join': False, 'storage_method': 'xep_0049' } def plugin_init(self): register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, Bookmarks) self.xmpp['xep_0049'].register(Bookmarks) self.xmpp['xep_0163'].register_pep('bookmarks', Bookmarks) self.xmpp.add_event_handler('session_start', self._autojoin) def plugin_end(self): self.xmpp.del_event_handler('session_start', self._autojoin) def _autojoin(self, __): if not self.auto_join: return try: result = self.get_bookmarks(method=self.storage_method) except XMPPError: return if self.storage_method == 'xep_0223': bookmarks = result['pubsub']['items']['item']['bookmarks'] else: bookmarks = result['private']['bookmarks'] for conf in bookmarks['conferences']: if conf['autojoin']: log.debug('Auto joining %s as %s', conf['jid'], conf['nick']) self.xmpp['xep_0045'].join_muc(conf['jid'], conf['nick'], password=conf['password']) def set_bookmarks(self, bookmarks, method=None, **iqargs): if not method: method = self.storage_method return self.xmpp[method].store(bookmarks, **iqargs) def get_bookmarks(self, method=None, **iqargs): if not method: method = self.storage_method loc = 'storage:bookmarks' if method == 'xep_0223' else 'bookmarks' return self.xmpp[method].retrieve(loc, **iqargs) slixmpp/slixmpp/plugins/xep_0048/stanza.py000066400000000000000000000036241477105560000210620ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp import JID from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Bookmarks(ElementBase): name = 'storage' namespace = 'storage:bookmarks' plugin_attrib = 'bookmarks' interfaces = set() def add_conference(self, jid, nick, name=None, autojoin=None, password=None): conf = Conference() conf['jid'] = jid conf['nick'] = nick if name is None: name = jid conf['name'] = name conf['autojoin'] = autojoin conf['password'] = password self.append(conf) def add_url(self, url, name=None): saved_url = URL() saved_url['url'] = url if name is None: name = url saved_url['name'] = name self.append(saved_url) class Conference(ElementBase): name = 'conference' namespace = 'storage:bookmarks' plugin_attrib = 'conference' plugin_multi_attrib = 'conferences' interfaces = {'nick', 'password', 'autojoin', 'jid', 'name'} sub_interfaces = {'nick', 'password'} def get_autojoin(self): value = self._get_attr('autojoin') return value in ('1', 'true') def set_autojoin(self, value): del self['autojoin'] 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' namespace = 'storage:bookmarks' plugin_attrib = 'url' plugin_multi_attrib = 'urls' interfaces = {'url', 'name'} register_stanza_plugin(Bookmarks, Conference, iterable=True) register_stanza_plugin(Bookmarks, URL, iterable=True) slixmpp/slixmpp/plugins/xep_0049/000077500000000000000000000000001477105560000172045ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0049/__init__.py000066400000000000000000000005571477105560000213240ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0049.stanza import PrivateXML from slixmpp.plugins.xep_0049.private_storage import XEP_0049 register_plugin(XEP_0049) slixmpp/slixmpp/plugins/xep_0049/private_storage.py000066400000000000000000000033421477105560000227560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from typing import ( List, Optional, Union, ) from asyncio import Future from slixmpp import JID from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, ElementBase from slixmpp.plugins.xep_0049 import stanza, PrivateXML log = logging.getLogger(__name__) class XEP_0049(BasePlugin): name = 'xep_0049' description = 'XEP-0049: Private XML Storage' dependencies = {} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, PrivateXML) def register(self, stanza): register_stanza_plugin(PrivateXML, stanza, iterable=True) def store(self, data: Union[List[ElementBase], ElementBase], ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Store data in Private XML Storage. :param data: An XML element or list of xml element to store. """ iq = self.xmpp.make_iq_set(ifrom=ifrom) if not isinstance(data, list): data = [data] for elem in data: iq['private'].append(elem) return iq.send(**iqkwargs) def retrieve(self, name: str, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Get previously stored data from Private XML Storage. :param name: Name of the payload to retrieve (slixmpp plugin attribute) """ iq = self.xmpp.make_iq_get(ifrom=ifrom) iq['private'].enable(name) return iq.send(**iqkwargs) slixmpp/slixmpp/plugins/xep_0049/stanza.py000066400000000000000000000005501477105560000210560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ET, ElementBase class PrivateXML(ElementBase): name = 'query' namespace = 'jabber:iq:private' plugin_attrib = 'private' interfaces = set() slixmpp/slixmpp/plugins/xep_0050/000077500000000000000000000000001477105560000171745ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0050/__init__.py000066400000000000000000000005421477105560000213060ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0050.stanza import Command from slixmpp.plugins.xep_0050.adhoc import XEP_0050 register_plugin(XEP_0050) slixmpp/slixmpp/plugins/xep_0050/adhoc.py000066400000000000000000000545121477105560000206330ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import functools import logging import time from slixmpp import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0050 import stanza from slixmpp.plugins.xep_0050 import Command from slixmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) class XEP_0050(BasePlugin): """ XEP-0050: Ad-Hoc Commands XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Events: command_execute -- Received a command with action="execute" command_next -- Received a command with action="next" command_complete -- Received a command with action="complete" command_cancel -- Received a command with action="cancel" Attributes: commands -- A dictionary mapping JID/node pairs to command names and handlers. sessions -- A dictionary or equivalent backend mapping session IDs to dictionaries containing data relevant to a command's session. """ name = 'xep_0050' description = 'XEP-0050: Ad-Hoc Commands' dependencies = {'xep_0030', 'xep_0004'} stanza = stanza default_config = { 'session_db': None } def plugin_init(self): """Start the XEP-0050 plugin.""" self.sessions = self.session_db if self.sessions is None: self.sessions = {} self.commands = {} self.xmpp.register_handler( Callback("Ad-Hoc Execute", StanzaPath('iq@type=set/command'), self._handle_command)) register_stanza_plugin(Iq, Command) register_stanza_plugin(Command, Form, iterable=True) self.xmpp.add_event_handler('command', self._handle_command_all) def plugin_end(self): self.xmpp.del_event_handler('command', self._handle_command_all) self.xmpp.remove_handler('Ad-Hoc Execute') self.xmpp['xep_0030'].del_feature(feature=Command.namespace) self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Command.namespace) self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def set_backend(self, db): """ Replace the default session storage dictionary with a generic, external data storage mechanism. The replacement backend must be able to interact through the same syntax and interfaces as a normal dictionary. :param db: The new session storage mechanism. """ self.sessions = db def prep_handlers(self, handlers, **kwargs): """ Prepare a list of functions for use by the backend service. Intended to be replaced by the backend service as needed. :param handlers: A list of function pointers :param kwargs: Any additional parameters required by the backend. """ pass # ================================================================= # Server side (command provider) API def add_command(self, jid=None, node=None, name='', handler=None): """ Make a new command available to external entities. Access control may be implemented in the provided handler. Command workflow is done across a sequence of command handlers. The first handler is given the initial Iq stanza of the request in order to support access control. Subsequent handlers are given only the payload items of the command. All handlers will receive the command's session data. :param jid: The JID that will expose the command. :param node: The node associated with the command. :param name: A human readable name for the command. :param handler: A function that will generate the response to the initial command request, as well as enforcing any access control policies. """ if jid is None: jid = self.xmpp.boundjid elif not isinstance(jid, JID): jid = JID(jid) item_jid = jid.full self.xmpp['xep_0030'].add_identity(category='automation', itype='command-list', name='Ad-Hoc commands', node=Command.namespace, jid=jid) self.xmpp['xep_0030'].add_item(jid=item_jid, name=name, node=Command.namespace, subnode=node, ijid=jid) self.xmpp['xep_0030'].add_identity(category='automation', itype='command-node', name=name, node=node, jid=jid) self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid) self.commands[(item_jid, node)] = (name, handler) def new_session(self): """Return a new session ID.""" return str(time.time()) + '-' + self.xmpp.new_id() def _handle_command(self, iq): """Raise command events based on the command action.""" self.xmpp.event('command', iq) self.xmpp.event('command_%s' % iq['command']['action'], iq) async def _handle_command_all(self, iq: Iq) -> None: action = iq['command']['action'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session is None: return await self._handle_command_start(iq) if action in ('next', 'execute'): return await self._handle_command_next(iq) if action == 'prev': return await self._handle_command_prev(iq) if action == 'complete': return await self._handle_command_complete(iq) if action == 'cancel': return await self._handle_command_cancel(iq) return None async def _handle_command_start(self, iq): """ Process an initial request to execute a command. :param iq: The command execution request. """ sessionid = self.new_session() node = iq['command']['node'] key = (iq['to'].full, node) name, handler = self.commands.get(key, ('Not found', None)) if not handler: log.debug('Command not found: %s, %s', key, self.commands) raise XMPPError('item-not-found') payload = [] for stanza in iq['command']['substanzas']: payload.append(stanza) if len(payload) == 1: payload = payload[0] interfaces = {item.plugin_attrib for item in payload} payload_classes = {item.__class__ for item in payload} initial_session = {'id': sessionid, 'from': iq['from'], 'to': iq['to'], 'node': node, 'payload': payload, 'interfaces': interfaces, 'payload_classes': payload_classes, 'notes': None, 'has_next': False, 'allow_complete': False, 'allow_prev': False, 'past': [], 'next': None, 'prev': None, 'cancel': None} session = await _await_if_needed(handler, iq, initial_session) self._process_command_response(iq, session) async def _handle_command_next(self, iq): """ Process a request for the next step in the workflow for a command with multiple steps. :param iq: The command continuation request. """ sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] session = await _await_if_needed(handler, results, session) self._process_command_response(iq, session) else: raise XMPPError('item-not-found') async def _handle_command_prev(self, iq): """ Process a request for the prev step in the workflow for a command with multiple steps. :param iq: The command continuation request. """ sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['prev'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] session = await _await_if_needed(handler, results, session) self._process_command_response(iq, session) else: raise XMPPError('item-not-found') def _process_command_response(self, iq, session): """ Generate a command reply stanza based on the provided session data. :param iq: The command request stanza. :param session: A dictionary of relevant session data. """ sessionid = session['id'] payload = session['payload'] if payload is None: payload = [] if not isinstance(payload, list): payload = [payload] interfaces = session.get('interfaces', set()) payload_classes = session.get('payload_classes', set()) interfaces.update({item.plugin_attrib for item in payload}) payload_classes.update({item.__class__ for item in payload}) session['interfaces'] = interfaces session['payload_classes'] = payload_classes self.sessions[sessionid] = session for item in payload: register_stanza_plugin(Command, item.__class__, iterable=True) iq = iq.reply() iq['command']['node'] = session['node'] iq['command']['sessionid'] = session['id'] if session['next'] is None: iq['command']['actions'] = [] iq['command']['status'] = 'completed' elif session['has_next']: actions = ['next'] if session['allow_complete']: actions.append('complete') if session['allow_prev']: actions.append('prev') iq['command']['actions'] = actions iq['command']['status'] = 'executing' else: actions = ['complete'] if session['allow_prev']: actions.append('prev') iq['command']['actions'] = actions iq['command']['status'] = 'executing' iq['command']['notes'] = session['notes'] for item in payload: iq['command'].append(item) iq.send() async def _handle_command_cancel(self, iq): """ Process a request to cancel a command's execution. :param iq: The command cancellation request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['cancel'] if handler: await _await_if_needed(handler, iq, session) del self.sessions[sessionid] iq = iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['status'] = 'canceled' iq['command']['notes'] = session['notes'] iq.send() else: raise XMPPError('item-not-found') async def _handle_command_complete(self, iq): """ Process a request to finish the execution of command and terminate the workflow. All data related to the command session will be removed. Arguments: :param iq: The command completion request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session: handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] if handler: await _await_if_needed(handler, results, session) del self.sessions[sessionid] payload = session['payload'] if payload is None: payload = [] if not isinstance(payload, list): payload = [payload] for item in payload: register_stanza_plugin(Command, item.__class__, iterable=True) iq = iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['actions'] = [] iq['command']['status'] = 'completed' iq['command']['notes'] = session['notes'] for item in payload: iq['command'].append(item) iq.send() else: raise XMPPError('item-not-found') # ================================================================= # Client side (command user) API def get_commands(self, jid, **kwargs): """ Return a list of commands provided by a given JID. :param jid: The JID to query for commands. :param local: If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. :param iterator: If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ return self.xmpp['xep_0030'].get_items(jid=jid, node=Command.namespace, **kwargs) def send_command(self, jid, node, ifrom=None, action='execute', payload=None, sessionid=None, flow=False, **kwargs): """ Create and send a command stanza, without using the provided workflow management APIs. :param jid: The JID to send the command request or result. :param node: The node for the command. :param ifrom: Specify the sender's JID. :param action: May be one of: execute, cancel, complete, or cancel. :param payload: Either a list of payload items, or a single payload item such as a data form. :param sessionid: The current session's ID value. :param flow: If True, process the Iq result using the command workflow methods contained in the session instead of returning the response stanza itself. Defaults to False. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = action if sessionid is not None: iq['command']['sessionid'] = sessionid if payload is not None: if not isinstance(payload, list): payload = [payload] for item in payload: iq['command'].append(item) if not flow: return iq.send(**kwargs) else: iq.send(callback=self._handle_command_result) def start_command(self, jid, node, session, ifrom=None): """ Initiate executing a command provided by a remote agent. The provided session dictionary should contain: :param next: A handler for processing the command result. :param error: A handler for processing any error stanzas generated by the request. :param jid: The JID to send the command request. :param node: The node for the desired command. :param session: A dictionary of relevant session data. """ session['jid'] = jid session['node'] = node session['timestamp'] = time.time() if 'payload' not in session: session['payload'] = None iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom session['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = 'execute' if session['payload'] is not None: payload = session['payload'] if not isinstance(payload, list): payload = list(payload) for stanza in payload: iq['command'].append(stanza) sessionid = 'client:pending_' + iq['id'] session['id'] = sessionid self.sessions[sessionid] = session iq.send(callback=self._handle_command_result) def continue_command(self, session, direction='next'): """ Execute the next action of the command. :param session: All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action=direction, payload=session.get('payload', None), sessionid=session['id'], flow=True) def cancel_command(self, session): """ Cancel the execution of a command. :param session: All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='cancel', payload=session.get('payload', None), sessionid=session['id'], flow=True) def complete_command(self, session): """ Finish the execution of a command workflow. :param session: All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='complete', payload=session.get('payload', None), sessionid=session['id'], flow=True) def terminate_command(self, session): """ Delete a command's session after a command has completed or an error has occurred. :param session: All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] try: del self.sessions[sessionid] except Exception as e: log.error("Error deleting adhoc command session: %s" % e) def _handle_command_result(self, iq): """ Process the results of a command request. Will execute the 'next' handler stored in the session data, or the 'error' handler depending on the Iq's type. :param iq: The command response. """ sessionid = 'client:' + iq['command']['sessionid'] pending = False if sessionid not in self.sessions: pending = True pendingid = 'client:pending_' + iq['id'] if pendingid not in self.sessions: return sessionid = pendingid session = self.sessions[sessionid] sessionid = 'client:' + iq['command']['sessionid'] session['id'] = iq['command']['sessionid'] self.sessions[sessionid] = session if pending: del self.sessions[pendingid] handler_type = 'next' if iq['type'] == 'error': handler_type = 'error' handler = session.get(handler_type, None) if handler: handler(iq, session) elif iq['type'] == 'error': self.terminate_command(session) if iq['command']['status'] == 'completed': self.terminate_command(session) def _iscoroutine_or_partial_coroutine(handler): return asyncio.iscoroutinefunction(handler) \ or isinstance(handler, functools.partial) \ and asyncio.iscoroutinefunction(handler.func) async def _await_if_needed(handler, *args): if handler is None: raise XMPPError("bad-request", text="The command is completed") if _iscoroutine_or_partial_coroutine(handler): log.debug(f"%s is async", handler) return await handler(*args) else: log.debug(f"%s is sync", handler) return handler(*args) slixmpp/slixmpp/plugins/xep_0050/stanza.py000066400000000000000000000130321477105560000210450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class Command(ElementBase): """ XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Example command stanzas: :: Information! Stanza Interface: :: action -- The action to perform. actions -- The set of allowable next actions. node -- The node associated with the command. notes -- A list of tuples for informative notes. sessionid -- A unique identifier for a command session. status -- May be one of: canceled, completed, or executing. """ name = 'command' namespace = 'http://jabber.org/protocol/commands' plugin_attrib = 'command' interfaces = {'action', 'sessionid', 'node', 'status', 'actions', 'notes'} actions = {'cancel', 'complete', 'execute', 'next', 'prev'} statuses = {'canceled', 'completed', 'executing'} next_actions = {'prev', 'next', 'complete'} def get_action(self): """ Return the value of the action attribute. If the Iq stanza's type is "set" then use a default value of "execute". """ if self.parent()['type'] == 'set': return self._get_attr('action', default='execute') return self._get_attr('action') def set_actions(self, values): """ Assign the set of allowable next actions. :param values: A list containing any combination of: 'prev', 'next', and 'complete' """ self.del_actions() if values: self._set_sub_text('{%s}actions' % self.namespace, '', True) actions = self.xml.find('{%s}actions' % self.namespace) for val in values: if val in self.next_actions: action = ET.Element('{%s}%s' % (self.namespace, val)) actions.append(action) def get_actions(self): """ Return the set of allowable next actions. """ actions = set() actions_xml = self.xml.find('{%s}actions' % self.namespace) if actions_xml is not None: for action in self.next_actions: action_xml = actions_xml.find('{%s}%s' % (self.namespace, action)) if action_xml is not None: actions.add(action) return actions def del_actions(self): """ Remove all allowable next actions. """ self._del_sub('{%s}actions' % self.namespace) def get_notes(self): """ Return a list of note information. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] """ notes = [] notes_xml = self.xml.findall('{%s}note' % self.namespace) for note in notes_xml: notes.append((note.attrib.get('type', 'info'), note.text)) return notes def set_notes(self, notes): """ Add multiple notes to the command result. Each note is a tuple, with the first item being one of: 'info', 'warning', or 'error', and the second item being any human readable message. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] Arguments: notes -- A list of tuples of note information. """ self.del_notes() for note in notes: self.add_note(note[1], note[0]) def del_notes(self): """ Remove all notes associated with the command result. """ notes_xml = self.xml.findall('{%s}note' % self.namespace) for note in notes_xml: self.xml.remove(note) def add_note(self, msg='', ntype='info'): """ Add a single note annotation to the command. Arguments: msg -- A human readable message. ntype -- One of: 'info', 'warning', 'error' """ xml = ET.Element('{%s}note' % self.namespace) xml.attrib['type'] = ntype xml.text = msg self.xml.append(xml) slixmpp/slixmpp/plugins/xep_0054/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0054/__init__.py000066400000000000000000000005511477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0054.stanza import VCardTemp from slixmpp.plugins.xep_0054.vcard_temp import XEP_0054 register_plugin(XEP_0054) slixmpp/slixmpp/plugins/xep_0054/stanza.py000066400000000000000000000354751477105560000210700ustar00rootroot00000000000000import base64 import datetime as dt from slixmpp.util import bytes from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID from slixmpp.plugins import xep_0082 class VCardTemp(ElementBase): name = 'vCard' namespace = 'vcard-temp' plugin_attrib = 'vcard_temp' interfaces = {'FN', 'VERSION'} sub_interfaces = {'FN', 'VERSION'} class Name(ElementBase): name = 'N' namespace = 'vcard-temp' plugin_attrib = name interfaces = {'FAMILY', 'GIVEN', 'MIDDLE', 'PREFIX', 'SUFFIX'} sub_interfaces = interfaces def _set_component(self, name, value): if isinstance(value, list): value = ','.join(value) if value is not None: self._set_sub_text(name, value, keep=True) else: self._del_sub(name) def _get_component(self, name): value = self._get_sub_text(name, '') if ',' in value: value = [v.strip() for v in value.split(',')] return value def set_family(self, value): self._set_component('FAMILY', value) def get_family(self): return self._get_component('FAMILY') def set_given(self, value): self._set_component('GIVEN', value) def get_given(self): return self._get_component('GIVEN') def set_middle(self, value): print(value) self._set_component('MIDDLE', value) def get_middle(self): return self._get_component('MIDDLE') def set_prefix(self, value): self._set_component('PREFIX', value) def get_prefix(self): return self._get_component('PREFIX') def set_suffix(self, value): self._set_component('SUFFIX', value) def get_suffix(self): return self._get_component('SUFFIX') class Nickname(ElementBase): name = 'NICKNAME' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'nicknames' interfaces = {name} is_extension = True def set_nickname(self, value): if not value: self.xml.text = '' return if not isinstance(value, list): value = [value] self.xml.text = ','.join(value) def get_nickname(self): if self.xml.text: return self.xml.text.split(',') class Email(ElementBase): name = 'EMAIL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'emails' interfaces = {'HOME', 'WORK', 'INTERNET', 'PREF', 'X400', 'USERID'} sub_interfaces = {'USERID'} bool_interfaces = {'HOME', 'WORK', 'INTERNET', 'PREF', 'X400'} class Address(ElementBase): name = 'ADR' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'addresses' interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL', 'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY'} sub_interfaces = {'POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY'} bool_interfaces = {'HOME', 'WORK', 'DOM', 'INTL', 'PREF'} class Telephone(ElementBase): name = 'TEL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'telephone_numbers' interfaces = {'HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF', 'NUMBER'} sub_interfaces = {'NUMBER'} bool_interfaces = {'HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF'} def setup(self, xml=None): super().setup(xml=xml) ## this blanks out numbers received from server ##self._set_sub_text('NUMBER', '', keep=True) def set_number(self, value): self._set_sub_text('NUMBER', value, keep=True) def del_number(self): self._set_sub_text('NUMBER', '', keep=True) class Label(ElementBase): name = 'LABEL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'labels' interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF', 'lines'} bool_interfaces = {'HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF'} def add_line(self, value): line = ET.Element('{%s}LINE' % self.namespace) line.text = value self.xml.append(line) def get_lines(self): lines = self.xml.find('{%s}LINE' % self.namespace) if lines is None: return [] return [line.text for line in lines] def set_lines(self, values): self.del_lines() for line in values: self.add_line(line) def del_lines(self): lines = self.xml.find('{%s}LINE' % self.namespace) if lines is None: return for line in lines: self.xml.remove(line) class Geo(ElementBase): name = 'GEO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'geolocations' interfaces = {'LAT', 'LON'} sub_interfaces = interfaces class Org(ElementBase): name = 'ORG' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'organizations' interfaces = {'ORGNAME', 'ORGUNIT', 'orgunits'} sub_interfaces = {'ORGNAME', 'ORGUNIT'} def add_orgunit(self, value): orgunit = ET.Element('{%s}ORGUNIT' % self.namespace) orgunit.text = value self.xml.append(orgunit) def get_orgunits(self): orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace) if orgunits is None: return [] return [orgunit.text for orgunit in orgunits] def set_orgunits(self, values): self.del_orgunits() for orgunit in values: self.add_orgunit(orgunit) def del_orgunits(self): orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace) if orgunits is None: return for orgunit in orgunits: self.xml.remove(orgunit) class Photo(ElementBase): name = 'PHOTO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'photos' interfaces = {'TYPE', 'EXTVAL'} sub_interfaces = interfaces class Logo(ElementBase): name = 'LOGO' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'logos' interfaces = {'TYPE', 'EXTVAL'} sub_interfaces = interfaces class Sound(ElementBase): name = 'SOUND' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'sounds' interfaces = {'PHONETC', 'EXTVAL'} sub_interfaces = interfaces class BinVal(ElementBase): name = 'BINVAL' namespace = 'vcard-temp' plugin_attrib = name interfaces = {'BINVAL'} is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_binval(self, value): self.del_binval() parent = self.parent() if value: xml = ET.Element('{%s}BINVAL' % self.namespace) xml.text = bytes(base64.b64encode(value)).decode('utf-8') parent.append(xml) def get_binval(self): parent = self.parent() xml = parent.xml.find('{%s}BINVAL' % self.namespace) if xml is not None: return base64.b64decode(bytes(xml.text)) return b'' def del_binval(self): self.parent()._del_sub('{%s}BINVAL' % self.namespace) class Classification(ElementBase): name = 'CLASS' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'classifications' interfaces = {'PUBLIC', 'PRIVATE', 'CONFIDENTIAL'} bool_interfaces = interfaces class Categories(ElementBase): name = 'CATEGORIES' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'categories' interfaces = {name} is_extension = True def set_categories(self, values): self.del_categories() for keyword in values: item = ET.Element('{%s}KEYWORD' % self.namespace) item.text = keyword self.xml.append(item) def get_categories(self): items = self.xml.findall('{%s}KEYWORD' % self.namespace) if items is None: return [] keywords = [] for item in items: keywords.append(item.text) return keywords def del_categories(self): items = self.xml.findall('{%s}KEYWORD' % self.namespace) for item in items: self.xml.remove(item) class Birthday(ElementBase): name = 'BDAY' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'birthdays' interfaces = {name} is_extension = True def set_bday(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self.xml.text = value def get_bday(self): if not self.xml.text: return None try: return xep_0082.parse(self.xml.text) except ValueError: return self.xml.text class Rev(ElementBase): name = 'REV' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'revision_dates' interfaces = {name} is_extension = True def set_rev(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self.xml.text = value def get_rev(self): if not self.xml.text: return None try: return xep_0082.parse(self.xml.text) except ValueError: return self.xml.text class Title(ElementBase): name = 'TITLE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'titles' interfaces = {name} is_extension = True def set_title(self, value): self.xml.text = value def get_title(self): return self.xml.text class Role(ElementBase): name = 'ROLE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'roles' interfaces = {name} is_extension = True def set_role(self, value): self.xml.text = value def get_role(self): return self.xml.text class Note(ElementBase): name = 'NOTE' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'notes' interfaces = {name} is_extension = True def set_note(self, value): self.xml.text = value def get_note(self): return self.xml.text class Desc(ElementBase): name = 'DESC' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'descriptions' interfaces = {name} is_extension = True def set_desc(self, value): self.xml.text = value def get_desc(self): return self.xml.text class URL(ElementBase): name = 'URL' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'urls' interfaces = {name} is_extension = True def set_url(self, value): self.xml.text = value def get_url(self): return self.xml.text class UID(ElementBase): name = 'UID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'uids' interfaces = {name} is_extension = True def set_uid(self, value): self.xml.text = value def get_uid(self): return self.xml.text class ProdID(ElementBase): name = 'PRODID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'product_ids' interfaces = {name} is_extension = True def set_prodid(self, value): self.xml.text = value def get_prodid(self): return self.xml.text class Mailer(ElementBase): name = 'MAILER' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'mailers' interfaces = {name} is_extension = True def set_mailer(self, value): self.xml.text = value def get_mailer(self): return self.xml.text class SortString(ElementBase): name = 'SORT-STRING' namespace = 'vcard-temp' plugin_attrib = 'SORT_STRING' plugin_multi_attrib = 'sort_strings' interfaces = {name} is_extension = True def set_sort_string(self, value): self.xml.text = value def get_sort_string(self): return self.xml.text class Agent(ElementBase): name = 'AGENT' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'agents' interfaces = {'EXTVAL'} sub_interfaces = interfaces class JabberID(ElementBase): name = 'JABBERID' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'jids' interfaces = {name} is_extension = True def set_jabberid(self, value): self.xml.text = JID(value).bare def get_jabberid(self): return JID(self.xml.text) class TimeZone(ElementBase): name = 'TZ' namespace = 'vcard-temp' plugin_attrib = name plugin_multi_attrib = 'timezones' interfaces = {name} is_extension = True def set_tz(self, value): time = xep_0082.time(offset=value) if time[-1] == 'Z': self.xml.text = 'Z' else: self.xml.text = time[-6:] def get_tz(self): if not self.xml.text: return dt.timezone.utc try: time = xep_0082.parse('00:00:00%s' % self.xml.text) return time.tzinfo except ValueError: return self.xml.text register_stanza_plugin(VCardTemp, Name) register_stanza_plugin(VCardTemp, Address, iterable=True) register_stanza_plugin(VCardTemp, Agent, iterable=True) register_stanza_plugin(VCardTemp, Birthday, iterable=True) register_stanza_plugin(VCardTemp, Categories, iterable=True) register_stanza_plugin(VCardTemp, Desc, iterable=True) register_stanza_plugin(VCardTemp, Email, iterable=True) register_stanza_plugin(VCardTemp, Geo, iterable=True) register_stanza_plugin(VCardTemp, JabberID, iterable=True) register_stanza_plugin(VCardTemp, Label, iterable=True) register_stanza_plugin(VCardTemp, Logo, iterable=True) register_stanza_plugin(VCardTemp, Mailer, iterable=True) register_stanza_plugin(VCardTemp, Note, iterable=True) register_stanza_plugin(VCardTemp, Nickname, iterable=True) register_stanza_plugin(VCardTemp, Org, iterable=True) register_stanza_plugin(VCardTemp, Photo, iterable=True) register_stanza_plugin(VCardTemp, ProdID, iterable=True) register_stanza_plugin(VCardTemp, Rev, iterable=True) register_stanza_plugin(VCardTemp, Role, iterable=True) register_stanza_plugin(VCardTemp, SortString, iterable=True) register_stanza_plugin(VCardTemp, Sound, iterable=True) register_stanza_plugin(VCardTemp, Telephone, iterable=True) register_stanza_plugin(VCardTemp, Title, iterable=True) register_stanza_plugin(VCardTemp, TimeZone, iterable=True) register_stanza_plugin(VCardTemp, UID, iterable=True) register_stanza_plugin(VCardTemp, URL, iterable=True) register_stanza_plugin(Photo, BinVal) register_stanza_plugin(Logo, BinVal) register_stanza_plugin(Sound, BinVal) register_stanza_plugin(Agent, VCardTemp) slixmpp/slixmpp/plugins/xep_0054/vcard_temp.py000066400000000000000000000121641477105560000217020ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from typing import Optional from slixmpp import JID from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0054 import VCardTemp, stanza log = logging.getLogger(__name__) class XEP_0054(BasePlugin): """ XEP-0054: vcard-temp """ name = 'xep_0054' description = 'XEP-0054: vcard-temp' dependencies = {'xep_0030', 'xep_0082'} stanza = stanza def plugin_init(self): """ Start the XEP-0054 plugin. """ register_stanza_plugin(Iq, VCardTemp) self.api.register(self._set_vcard, 'set_vcard', default=True) self.api.register(self._get_vcard, 'get_vcard', default=True) self.api.register(self._del_vcard, 'del_vcard', default=True) self._vcard_cache = {} self.xmpp.register_handler( CoroutineCallback('VCardTemp', StanzaPath('iq/vcard_temp'), self._handle_get_vcard)) def plugin_end(self): self.xmpp.remove_handler('VCardTemp') self.xmpp['xep_0030'].del_feature(feature='vcard-temp') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('vcard-temp') def make_vcard(self) -> VCardTemp: """Return an empty vcard element.""" return VCardTemp() async def get_vcard(self, jid: Optional[JID] = None, *, local: Optional[bool] = None, cached: bool = False, ifrom: Optional[JID] = None, **iqkwargs) -> Iq: """Retrieve a VCard. .. versionchanged:: 1.8.0 This function is now a coroutine. :param jid: JID of the entity to fetch the VCard from. :param local: Only check internally for a vcard. :param cached: Whether to check in the local cache before sending a query. """ if local is None: if jid is not None and not isinstance(jid, JID): jid = JID(jid) if self.xmpp.is_component: if jid.domain == self.xmpp.boundjid.domain: local = True else: if str(jid) == str(self.xmpp.boundjid): local = True jid = jid.full elif jid in (None, ''): local = True if local: vcard = await self.api['get_vcard'](jid, None, ifrom) if not isinstance(vcard, Iq): iq = self.xmpp.Iq() if vcard is None: vcard = VCardTemp() iq.append(vcard) return iq return vcard if cached: vcard = await self.api['get_vcard'](jid, None, ifrom) if vcard is not None: if not isinstance(vcard, Iq): iq = self.xmpp.Iq() iq.append(vcard) return iq return vcard iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) iq.enable('vcard_temp') return await iq.send(**iqkwargs) async def publish_vcard(self, vcard: Optional[VCardTemp] = None, jid: Optional[JID] = None, ifrom: Optional[JID] = None, **iqkwargs): """Publish a vcard. .. versionchanged:: 1.8.0 This function is now a coroutine. :param vcard: The VCard to publish. :param jid: The JID to publish the VCard to. """ await self.api['set_vcard'](jid, None, ifrom, vcard) if self.xmpp.is_component: return iq = self.xmpp.make_iq_set(ito=jid, ifrom=ifrom) iq.append(vcard) await iq.send(**iqkwargs) async def _handle_get_vcard(self, iq: Iq): if iq['type'] == 'result': await self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp']) return elif iq['type'] == 'get' and self.xmpp.is_component: vcard = await self.api['get_vcard'](iq['to'].bare, ifrom=iq['from']) if vcard is None: raise XMPPError("item-not-found") elif isinstance(vcard, Iq): await vcard.send() else: iq = iq.reply() iq.append(vcard) iq.send() elif iq['type'] == 'set': raise XMPPError('service-unavailable') # ================================================================= def _set_vcard(self, jid, node, ifrom, vcard): self._vcard_cache[jid.bare] = vcard def _get_vcard(self, jid, node, ifrom, vcard): return self._vcard_cache.get(jid.bare, None) def _del_vcard(self, jid, node, ifrom, vcard): if jid.bare in self._vcard_cache: del self._vcard_cache[jid.bare] slixmpp/slixmpp/plugins/xep_0055/000077500000000000000000000000001477105560000172015ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0055/__init__.py000066400000000000000000000001531477105560000213110ustar00rootroot00000000000000from slixmpp.plugins.base import register_plugin from .search import XEP_0055 register_plugin(XEP_0055) slixmpp/slixmpp/plugins/xep_0055/search.py000066400000000000000000000053641477105560000210300ustar00rootroot00000000000000import logging from slixmpp import CoroutineCallback, StanzaPath, Iq, register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import StanzaBase from . import stanza class XEP_0055(BasePlugin): """ XEP-0055: Jabber Search The config options are only useful for a "server-side" search feature, and if the ``provide_search`` option is set to True. API === ``search_get_form``: customize the search form content (ie fields) ``search_query``: return search results """ name = "xep_0055" description = "XEP-0055: Jabber search" dependencies = {"xep_0004", "xep_0030"} stanza = stanza default_config = { "form_fields": {"first", "last"}, "form_instructions": "", "form_title": "", "provide_search": True } def plugin_init(self): register_stanza_plugin(Iq, stanza.Search) register_stanza_plugin(stanza.Search, self.xmpp["xep_0004"].stanza.Form) if self.provide_search: self.xmpp["xep_0030"].add_feature(stanza.Search.namespace) self.xmpp.register_handler( CoroutineCallback( "search", StanzaPath("/iq/search"), self._handle_search, ) ) self.api.register(self._get_form, "search_get_form") self.api.register(self._get_results, "search_query") async def _handle_search(self, iq: StanzaBase): if iq["search"]["form"].get_values(): reply = await self.api["search_query"](None, None, iq.get_from(), iq) reply["search"]["form"]["type"] = "result" else: reply = await self.api["search_get_form"](None, None, iq.get_from(), iq) reply["search"]["form"].add_field( "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden" ) reply.send() async def _get_form(self, jid, node, ifrom, iq): reply = iq.reply() form = reply["search"]["form"] form["title"] = self.form_title form["instructions"] = self.form_instructions for field in self.form_fields: form.add_field(field) return reply async def _get_results(self, jid, node, ifrom, iq): reply = iq.reply() form = reply["search"]["form"] form["type"] = "result" for field in self.form_fields: form.add_reported(field) return reply def make_search_iq(self, **kwargs): iq = self.xmpp.make_iq(itype="set", **kwargs) iq["search"]["form"].set_type("submit") iq["search"]["form"].add_field( "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden" ) return iq log = logging.getLogger(__name__) slixmpp/slixmpp/plugins/xep_0055/stanza.py000066400000000000000000000003471477105560000210570ustar00rootroot00000000000000from typing import Set, ClassVar from slixmpp.xmlstream import ElementBase class Search(ElementBase): namespace = "jabber:iq:search" name = "query" plugin_attrib = "search" interfaces: ClassVar[Set[str]] = set() slixmpp/slixmpp/plugins/xep_0059/000077500000000000000000000000001477105560000172055ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0059/__init__.py000066400000000000000000000005631477105560000213220ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0059.stanza import Set from slixmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059 register_plugin(XEP_0059) slixmpp/slixmpp/plugins/xep_0059/rsm.py000066400000000000000000000176421477105560000203720ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from collections.abc import AsyncIterator from typing import ( Any, Callable, Dict, Optional, ) from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0059 import stanza, Set from slixmpp.exceptions import XMPPError log = logging.getLogger(__name__) class ResultIterator(AsyncIterator): """ An iterator for Result Set Management Example: .. code-block:: python q = Iq() q['to'] = 'pubsub.example.com' q['disco_items']['node'] = 'blog' async for i in ResultIterator(q, 'disco_items', '10'): print(i['disco_items']['items']) """ #: Template for the RSM query query: Iq #: Substanza of the query to send, e.g. "disco_items" interface: str #: Stanza interface on the query results providing the retrieved #: elements (used to count them) results: str #: From which item id to start start: Optional[str] #: Amount of elements to retrieve for each page amount: int #: If True, page backwards through the results reverse: bool #: Callback to run before sending the stanza pre_cb: Optional[Callable[[Iq], None]] #: Callback to run after receiving the reply post_cb: Optional[Callable[[Iq], None]] #: Optional dict of Iq options (timeout, etc…) for Iq.send() iq_options: Dict[str, Any] def __init__(self, query: Iq, interface: str, results: str = 'substanzas', amount: int = 10, start: Optional[str] = None, reverse: bool = False, recv_interface: Optional[str] = None, pre_cb: Optional[Callable[[Iq], None]] = None, post_cb: Optional[Callable[[Iq], None]] = None, iq_options: Optional[Dict[str, Any]] = None): """ :param query: The template query :param interface: The substanza of the query to send, for example disco_items :param recv_interface: The substanza of the query to receive, for example disco_items :param results: The query stanza's interface which provides a countable list of query results. :param amount: The max amounts of items to request per iteration :param start: From which item id to start :param reverse: If True, page backwards through the results :param pre_cb: Callback to run before sending the stanza :param post_cb: Callback to run after receiving the reply :param iq_options: Optional dict of parameters for Iq.send """ self.query = query self.amount = amount self.start = start if iq_options is None: self.iq_options = {} else: self.iq_options = iq_options self.interface = interface if recv_interface is not None: self.recv_interface = recv_interface else: self.recv_interface = interface self.pre_cb = pre_cb self.post_cb = post_cb self.results = results self.reverse = reverse self._stop = False def __aiter__(self): return self async def __anext__(self) -> Iq: return await self.next() async def next(self) -> Iq: """ Return the next page of results from a query. Note: If using backwards paging, then the next page of results will be the items before the current page of items. """ if self._stop: raise StopAsyncIteration self.query['id'] = self.query.stream.new_id() self.query[self.interface]['rsm']['max'] = str(self.amount) if self.start: if self.reverse: self.query[self.interface]['rsm']['before'] = self.start else: self.query[self.interface]['rsm']['after'] = self.start elif self.reverse: self.query[self.interface]['rsm']['before'] = True try: if self.pre_cb: self.pre_cb(self.query) r = await self.query.send(**self.iq_options) if not r[self.recv_interface]['rsm']['first'] and \ not r[self.recv_interface]['rsm']['last']: raise StopAsyncIteration if self.post_cb: self.post_cb(r) if r[self.recv_interface]['rsm']['count'] and \ r[self.recv_interface]['rsm']['first_index']: count = int(r[self.recv_interface]['rsm']['count']) first = int(r[self.recv_interface]['rsm']['first_index']) num_items = len(r[self.recv_interface][self.results]) if first + num_items == count: self._stop = True if self.reverse: self.start = r[self.recv_interface]['rsm']['first'] else: self.start = r[self.recv_interface]['rsm']['last'] return r except XMPPError: raise StopAsyncIteration class XEP_0059(BasePlugin): """ XEP-0059: Result Set Management """ name = 'xep_0059' description = 'XEP-0059: Result Set Management' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): """ Start the XEP-0059 plugin. """ register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems, self.stanza.Set) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Set.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Set.namespace) def iterate(self, stanza: Iq, interface: str, results: str = 'substanzas', amount: int = 10, reverse: bool = False, recv_interface: Optional[str] = None, pre_cb: Optional[Callable[[Iq], None]] = None, post_cb: Optional[Callable[[Iq], None]] = None, iq_options: Optional[Dict[str, Any]] = None ) -> ResultIterator: """ Create a new result set iterator for a given stanza query. :param stanza: A stanza object to serve as a template for queries made each iteration. For example, a basic disco#items query. :param interface: The name of the substanza to which the result set management stanza should be appended in the query stanza. For example, for disco#items queries the interface 'disco_items' should be used. :param recv_interface: The name of the substanza from which the result set management stanza should be read in the result stanza. If unspecified, it will be set to the same value as the ``interface`` parameter. :param pre_cb: Callback to run before sending each stanza e.g. setting the MAM queryid and starting a stanza collector. :param post_cb: Callback to run after receiving each stanza e.g. stopping a MAM stanza collector in order to gather results. :param results: The name of the interface containing the query results (typically just 'substanzas'). :param iq_options: Optional dict of parameters for Iq.send """ return ResultIterator(stanza, interface, results, amount, reverse=reverse, recv_interface=recv_interface, pre_cb=pre_cb, post_cb=post_cb, iq_options=iq_options) slixmpp/slixmpp/plugins/xep_0059/stanza.py000066400000000000000000000073501477105560000210640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins.xep_0030.stanza.items import DiscoItems class Set(ElementBase): """ XEP-0059 (Result Set Management) can be used to manage the results of queries. For example, limiting the number of items per response or starting at certain positions. Example set stanzas: :: 2 conference.example.com pubsub.example.com Stanza Interface: :: first_index -- The index attribute of after -- The id defining from which item to start before -- The id defining from which item to start when browsing backwards max -- Max amount per response first -- Id for the first item in the response last -- Id for the last item in the response index -- Used to set an index to start from count -- The number of remote items available """ namespace = 'http://jabber.org/protocol/rsm' name = 'set' plugin_attrib = 'rsm' sub_interfaces = {'first', 'after', 'before', 'count', 'index', 'last', 'max'} interfaces = {'first_index', 'first', 'after', 'before', 'count', 'index', 'last', 'max'} def set_first_index(self, val): """ Sets the index attribute for and creates the element if it doesn't exist """ fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: if val: fi.attrib['index'] = val elif 'index' in fi.attrib: del fi.attrib['index'] elif val: fi = ET.Element("{%s}first" % (self.namespace)) fi.attrib['index'] = val self.xml.append(fi) def get_first_index(self): """ Returns the value of the index attribute for """ fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: return fi.attrib.get('index', '') def del_first_index(self): """ Removes the index attribute for but keeps the element """ fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: del fi.attrib['index'] def set_before(self, val): """ Sets the value of , if the value is True then the element will be created without a value """ b = self.xml.find("{%s}before" % (self.namespace)) if b is None and val is True: self._set_sub_text('{%s}before' % self.namespace, '', True) else: self._set_sub_text('{%s}before' % self.namespace, val) def get_before(self): """ Returns the value of , if it is empty it will return True """ b = self.xml.find("{%s}before" % (self.namespace)) if b is not None and not b.text: return True elif b is not None: return b.text else: return None slixmpp/slixmpp/plugins/xep_0060/000077500000000000000000000000001477105560000171755ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0060/__init__.py000066400000000000000000000005331477105560000213070ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0060.pubsub import XEP_0060 from slixmpp.plugins.xep_0060 import stanza register_plugin(XEP_0060) slixmpp/slixmpp/plugins/xep_0060/pubsub.py000066400000000000000000000467101477105560000210570ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.xmlstream import JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0060 import stanza log = logging.getLogger(__name__) class XEP_0060(BasePlugin): """ XEP-0060 Publish Subscribe """ name = 'xep_0060' description = 'XEP-0060: Publish-Subscribe' dependencies = {'xep_0030', 'xep_0004', 'xep_0082', 'xep_0131'} stanza = stanza def plugin_init(self): self.node_event_map = {} self.xmpp.register_handler( Callback('Pubsub Event: Items', StanzaPath('message/pubsub_event/items'), self._handle_event_items)) self.xmpp.register_handler( Callback('Pubsub Event: Purge', StanzaPath('message/pubsub_event/purge'), self._handle_event_purge)) self.xmpp.register_handler( Callback('Pubsub Event: Delete', StanzaPath('message/pubsub_event/delete'), self._handle_event_delete)) self.xmpp.register_handler( Callback('Pubsub Event: Configuration', StanzaPath('message/pubsub_event/configuration'), self._handle_event_configuration)) self.xmpp.register_handler( Callback('Pubsub Event: Subscription', StanzaPath('message/pubsub_event/subscription'), self._handle_event_subscription)) self.xmpp['xep_0131'].supported_headers.add('SubID') def plugin_end(self): self.xmpp.remove_handler('Pubsub Event: Items') self.xmpp.remove_handler('Pubsub Event: Purge') self.xmpp.remove_handler('Pubsub Event: Delete') self.xmpp.remove_handler('Pubsub Event: Configuration') self.xmpp.remove_handler('Pubsub Event: Subscription') def _handle_event_items(self, msg): """Raise events for publish and retraction notifications.""" node = msg['pubsub_event']['items']['node'] multi = len(msg['pubsub_event']['items']) > 1 values = {} if multi: values = msg.values del values['pubsub_event'] for item in msg['pubsub_event']['items']: event_name = self.node_event_map.get(node, None) event_type = 'publish' if item.name == 'retract': event_type = 'retract' if multi: condensed = self.xmpp.Message() condensed.values = values condensed['pubsub_event']['items']['node'] = node condensed['pubsub_event']['items'].append(item) self.xmpp.event('pubsub_%s' % event_type, msg) if event_name: self.xmpp.event('%s_%s' % (event_name, event_type), condensed) else: self.xmpp.event('pubsub_%s' % event_type, msg) if event_name: self.xmpp.event('%s_%s' % (event_name, event_type), msg) def _handle_event_purge(self, msg): """Raise events for node purge notifications.""" node = msg['pubsub_event']['purge']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_purge', msg) if event_name: self.xmpp.event('%s_purge' % event_name, msg) def _handle_event_delete(self, msg): """Raise events for node deletion notifications.""" node = msg['pubsub_event']['delete']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_delete', msg) if event_name: self.xmpp.event('%s_delete' % event_name, msg) def _handle_event_configuration(self, msg): """Raise events for node configuration notifications.""" node = msg['pubsub_event']['configuration']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_config', msg) if event_name: self.xmpp.event('%s_config' % event_name, msg) def _handle_event_subscription(self, msg): """Raise events for node subscription notifications.""" node = msg['pubsub_event']['subscription']['node'] event_name = self.node_event_map.get(node, None) self.xmpp.event('pubsub_subscription', msg) if event_name: self.xmpp.event('%s_subscription' % event_name, msg) def map_node_event(self, node, event_name): """ Map node names to events. When a pubsub event is received for the given node, raise the provided event. For example:: map_node_event('http://jabber.org/protocol/tune', 'user_tune') will produce the events 'user_tune_publish' and 'user_tune_retract' when the respective notifications are received from the node 'http://jabber.org/protocol/tune', among other events. :param node: The node name to map to an event. :param event_name: The name of the event to raise when a notification from the given node is received. """ self.node_event_map[node] = event_name def create_node(self, jid, node, config=None, ntype=None, ifrom=None, callback=None, timeout=None): """ Create and configure a new pubsub node. A server MAY use a different name for the node than the one provided, so be sure to check the result stanza for a server assigned name. If no configuration form is provided, the node will be created using the server's default configuration. To get the default configuration use get_node_config(). :param jid: The JID of the pubsub service. :param node: Optional name of the node to create. If no name is provided, the server MAY generate a node ID for you. The server can also assign a different name than the one you provide; check the result stanza to see if the server assigned a name. :param config: Optional XEP-0004 data form of configuration settings. :param ntype: The type of node to create. Servers typically default to using 'leaf' if no type is provided. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['create']['node'] = node if config is not None: form_type = 'http://jabber.org/protocol/pubsub#node_config' if 'FORM_TYPE' in config.get_fields(): config.field['FORM_TYPE']['value'] = form_type else: config.add_field(var='FORM_TYPE', ftype='hidden', value=form_type) if ntype: if 'pubsub#node_type' in config.get_fields(): config.field['pubsub#node_type']['value'] = ntype else: config.add_field(var='pubsub#node_type', value=ntype) iq['pubsub']['configure'].append(config) return iq.send(callback=callback, timeout=timeout) def subscribe(self, jid, node, bare=True, subscribee=None, options=None, ifrom=None, callback=None, timeout=None): """ Subscribe to updates from a pubsub node. The rules for determining the JID that is subscribing to the node are: 1. If subscribee is given, use that as provided. 2. If ifrom was given, use the bare or full version based on bare. 3. Otherwise, use self.xmpp.boundjid based on bare. :param jid: The pubsub service JID. :param node: The node to subscribe to. :param bare: Indicates if the subscribee is a bare or full JID. Defaults to True for a bare JID. :param subscribee: The JID that is subscribing to the node. :param options: """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['subscribe']['node'] = node if subscribee is None: if ifrom: if bare: subscribee = JID(ifrom).bare else: subscribee = ifrom else: if bare: subscribee = self.xmpp.boundjid.bare else: subscribee = self.xmpp.boundjid iq['pubsub']['subscribe']['jid'] = subscribee if options is not None: iq['pubsub']['options'].append(options) return iq.send(callback=callback, timeout=timeout) def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None, ifrom=None, callback=None, timeout=None): """ Unubscribe from updates from a pubsub node. The rules for determining the JID that is unsubscribing from the node are: 1. If subscribee is given, use that as provided. 2. If ifrom was given, use the bare or full version based on bare. 3. Otherwise, use self.xmpp.boundjid based on bare. :param jid: The pubsub service JID. :param node: The node to unsubscribe from. :param subid: The specific subscription, if multiple subscriptions exist for this JID/node combination. :param bare: Indicates if the subscribee is a bare or full JID. Defaults to True for a bare JID. :param subscribee: The JID that is unsubscribing from the node. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['unsubscribe']['node'] = node if subscribee is None: if ifrom: if bare: subscribee = JID(ifrom).bare else: subscribee = ifrom else: if bare: subscribee = self.xmpp.boundjid.bare else: subscribee = self.xmpp.boundjid iq['pubsub']['unsubscribe']['jid'] = subscribee iq['pubsub']['unsubscribe']['subid'] = subid return iq.send(callback=callback, timeout=timeout) def get_subscriptions(self, jid, node=None, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['subscriptions']['node'] = node return iq.send(callback=callback, timeout=timeout) def get_affiliations(self, jid, node=None, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['affiliations']['node'] = node return iq.send(callback=callback, timeout=timeout) def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') if user_jid is None: iq['pubsub']['default']['node'] = node else: iq['pubsub']['options']['node'] = node iq['pubsub']['options']['jid'] = user_jid return iq.send(callback=callback, timeout=timeout) def set_subscription_options(self, jid, node, user_jid, options, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['options']['node'] = node iq['pubsub']['options']['jid'] = user_jid iq['pubsub']['options'].append(options) return iq.send(callback=callback, timeout=timeout) def get_node_config(self, jid, node=None, ifrom=None, callback=None, timeout=None): """ Retrieve the configuration for a node, or the pubsub service's default configuration for new nodes. :param jid: The JID of the pubsub service. :param node: The node to retrieve the configuration for. If None, the default configuration for new nodes will be requested. Defaults to None. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') if node is None: iq['pubsub_owner']['default'] else: iq['pubsub_owner']['configure']['node'] = node return iq.send(callback=callback, timeout=timeout) def get_node_subscriptions(self, jid, node, ifrom=None, callback=None, timeout=None): """ Retrieve the subscriptions associated with a given node. :param jid: The JID of the pubsub service. :param node: The node to retrieve subscriptions from. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub_owner']['subscriptions']['node'] = node return iq.send(callback=callback, timeout=timeout) def get_node_affiliations(self, jid, node, ifrom=None, callback=None, timeout=None): """ Retrieve the affiliations associated with a given node. :param jid: The JID of the pubsub service. :param node: The node to retrieve affiliations from. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub_owner']['affiliations']['node'] = node return iq.send(callback=callback, timeout=timeout) def delete_node(self, jid, node, ifrom=None, callback=None, timeout=None): """ Delete a a pubsub node. :param jid: The JID of the pubsub service. :param node: The node to delete. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['delete']['node'] = node return iq.send(callback=callback, timeout=timeout) def set_node_config(self, jid, node, config, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['configure']['node'] = node iq['pubsub_owner']['configure'].append(config) return iq.send(callback=callback, timeout=timeout) def publish(self, jid, node, id=None, payload=None, options=None, ifrom=None, callback=None, timeout=None): """ Add a new item to a node, or edit an existing item. For services that support it, you can use the publish command as an event signal by not including an ID or payload. When including a payload and you do not provide an ID then the service will generally create an ID for you. Publish options may be specified, and how those options are processed is left to the service, such as treating the options as preconditions that the node's settings must match. :param jid: The JID of the pubsub service. :param node: The node to publish the item to. :param id: Optionally specify the ID of the item. :param payload: The item content to publish. :param options: A form of publish options. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['publish']['node'] = node if id is not None: iq['pubsub']['publish']['item']['id'] = id if payload is not None: iq['pubsub']['publish']['item']['payload'] = payload iq['pubsub']['publish_options'] = options return iq.send(callback=callback, timeout=timeout) def retract(self, jid, node, id, notify=None, ifrom=None, callback=None, timeout=None): """ Delete a single item from a node. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub']['retract']['node'] = node iq['pubsub']['retract']['notify'] = notify iq['pubsub']['retract']['item']['id'] = id return iq.send(callback=callback, timeout=timeout) def purge(self, jid, node, ifrom=None, callback=None, timeout=None): """ Remove all items from a node. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['purge']['node'] = node return iq.send(callback=callback, timeout=timeout) def get_nodes(self, *args, **kwargs): """ Discover the nodes provided by a Pubsub service, using disco. """ return self.xmpp['xep_0030'].get_items(*args, **kwargs) def get_item(self, jid, node, item_id, ifrom=None, callback=None, timeout=None): """ Retrieve the content of an individual item. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') item = stanza.Item() item['id'] = item_id iq['pubsub']['items']['node'] = node iq['pubsub']['items'].append(item) return iq.send(callback=callback, timeout=timeout) def get_items(self, jid, node, item_ids=None, max_items=None, iterator=False, ifrom=None, callback=None, timeout=None): """ Request the contents of a node's items. The desired items can be specified, or a query for the last few published items can be used. Pubsub services may use result set management for nodes with many items, so an iterator can be returned if needed. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') iq['pubsub']['items']['node'] = node iq['pubsub']['items']['max_items'] = max_items if item_ids is not None: for item_id in item_ids: item = stanza.Item() item['id'] = item_id iq['pubsub']['items'].append(item) if iterator: return self.xmpp['xep_0059'].iterate(iq, 'pubsub') else: return iq.send(callback=callback, timeout=timeout) def get_item_ids(self, jid, node, ifrom=None, callback=None, timeout=None, iterator=False): """ Retrieve the ItemIDs hosted by a given node, using disco. """ return self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom, callback=callback, timeout=timeout, iterator=iterator) def modify_affiliations(self, jid, node, affiliations=None, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['affiliations']['node'] = node if affiliations is None: affiliations = [] for jid, affiliation in affiliations: aff = stanza.OwnerAffiliation() aff['jid'] = jid aff['affiliation'] = affiliation iq['pubsub_owner']['affiliations'].append(aff) return iq.send(callback=callback, timeout=timeout) def modify_subscriptions(self, jid, node, subscriptions=None, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['subscriptions']['node'] = node if subscriptions is None: subscriptions = [] for jid, subscription in subscriptions: sub = stanza.OwnerSubscription() sub['jid'] = jid sub['subscription'] = subscription iq['pubsub_owner']['subscriptions'].append(sub) return iq.send(callback=callback, timeout=timeout) slixmpp/slixmpp/plugins/xep_0060/stanza/000077500000000000000000000000001477105560000204755ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0060/stanza/__init__.py000066400000000000000000000006021477105560000226040ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.xep_0060.stanza.pubsub import * from slixmpp.plugins.xep_0060.stanza.pubsub_owner import * from slixmpp.plugins.xep_0060.stanza.pubsub_event import * from slixmpp.plugins.xep_0060.stanza.pubsub_errors import * slixmpp/slixmpp/plugins/xep_0060/stanza/base.py000066400000000000000000000014441477105560000217640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ET class OptionalSetting: interfaces = {'required'} xml: ET.Element namespace: str def set_required(self, value): if value in (True, 'true', 'True', '1'): self.xml.append(ET.Element("{%s}required" % self.namespace)) elif self.get_required(): self.del_required() def get_required(self): required = self.xml.find("{%s}required" % self.namespace) return required is not None def del_required(self): required = self.xml.find("{%s}required" % self.namespace) if required is not None: self.xml.remove(required) slixmpp/slixmpp/plugins/xep_0060/stanza/pubsub.py000066400000000000000000000165671477105560000223660ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp import Iq, Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins import xep_0004 from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting class Pubsub(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'pubsub' plugin_attrib = name interfaces = set(tuple()) class Affiliations(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliations' plugin_attrib = name interfaces = {'node'} class Affiliation(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliation' plugin_attrib = name interfaces = {'node', 'affiliation', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscription' plugin_attrib = name interfaces = {'jid', 'node', 'subscription', 'subid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscriptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscriptions' plugin_attrib = name interfaces = {'node'} class SubscribeOptions(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe-options' plugin_attrib = 'suboptions' interfaces = {'required'} class Item(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'item' plugin_attrib = name interfaces = {'id', 'payload'} def set_payload(self, value): del self['payload'] if isinstance(value, ElementBase): if value.tag_name() in self.plugin_tag_map: self.init_plugin(value.plugin_attrib, existing_xml=value.xml) self.xml.append(value.xml) else: self.xml.append(value) def get_payload(self): children = list(self.xml) if len(children) > 0: return children[0] def del_payload(self): for child in self.xml: self.xml.remove(child) class Items(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'items' plugin_attrib = name interfaces = {'node', 'max_items'} def set_max_items(self, value): self._set_attr('max_items', str(value)) class Create(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'create' plugin_attrib = name interfaces = {'node'} class Default(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'default' plugin_attrib = name interfaces = {'node', 'type'} def get_type(self): t = self._get_attr('type') if not t: return 'leaf' return t class Publish(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish' plugin_attrib = name interfaces = {'node'} class Retract(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'retract' plugin_attrib = name interfaces = {'node', 'notify'} def get_notify(self): notify = self._get_attr('notify') if notify in ('0', 'false'): return False elif notify in ('1', 'true'): return True return None def set_notify(self, value): del self['notify'] if value is None: return elif value in (True, '1', 'true', 'True'): self._set_attr('notify', 'true') else: self._set_attr('notify', 'false') class Unsubscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'unsubscribe' plugin_attrib = name interfaces = {'node', 'jid', 'subid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Subscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe' plugin_attrib = name interfaces = {'node', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class Configure(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'configure' plugin_attrib = name interfaces = {'node', 'type'} def getType(self): t = self._get_attr('type') if not t: t == 'leaf' return t class Options(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'options' plugin_attrib = name interfaces = {'jid', 'node', 'options'} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def get_options(self): config = self.xml.find('{jabber:x:data}x') form = xep_0004.Form(xml=config) return form def set_options(self, value): if isinstance(value, ElementBase): self.xml.append(value.xml) else: self.xml.append(value) return self def del_options(self): config = self.xml.find('{jabber:x:data}x') self.xml.remove(config) def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class PublishOptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish-options' plugin_attrib = 'publish_options' interfaces = {'publish_options'} is_extension = True def get_publish_options(self): config = self.xml.find('{jabber:x:data}x') if config is None: return None form = xep_0004.Form(xml=config) return form def set_publish_options(self, value): if value is None: self.del_publish_options() else: if isinstance(value, ElementBase): self.xml.append(value.xml) else: self.xml.append(value) return self def del_publish_options(self): config = self.xml.find('{jabber:x:data}x') if config is not None: self.xml.remove(config) self.parent().xml.remove(self.xml) register_stanza_plugin(Iq, Pubsub) register_stanza_plugin(Pubsub, Affiliations) register_stanza_plugin(Pubsub, Configure) register_stanza_plugin(Pubsub, Create) register_stanza_plugin(Pubsub, Default) register_stanza_plugin(Pubsub, Items) register_stanza_plugin(Pubsub, Options) register_stanza_plugin(Pubsub, Publish) register_stanza_plugin(Pubsub, PublishOptions) register_stanza_plugin(Pubsub, Retract) register_stanza_plugin(Pubsub, Subscribe) register_stanza_plugin(Pubsub, Subscription) register_stanza_plugin(Pubsub, Subscriptions) register_stanza_plugin(Pubsub, Unsubscribe) register_stanza_plugin(Affiliations, Affiliation, iterable=True) register_stanza_plugin(Configure, xep_0004.Form) register_stanza_plugin(Items, Item, iterable=True) register_stanza_plugin(Publish, Item, iterable=True) register_stanza_plugin(Retract, Item) register_stanza_plugin(Subscribe, Options) register_stanza_plugin(Subscription, SubscribeOptions) register_stanza_plugin(Subscriptions, Subscription, iterable=True) slixmpp/slixmpp/plugins/xep_0060/stanza/pubsub_errors.py000066400000000000000000000057311477105560000237510ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class PubsubErrorCondition(ElementBase): plugin_attrib = 'pubsub' interfaces = {'condition', 'unsupported'} plugin_attrib_map = {} plugin_tag_map = {} conditions = {'closed-node', 'configuration-required', 'invalid-jid', 'invalid-options', 'invalid-payload', 'invalid-subid', 'item-forbidden', 'item-required', 'jid-required', 'max-items-exceeded', 'max-nodes-exceeded', 'nodeid-required', 'not-in-roster-group', 'not-subscribed', 'payload-too-big', 'payload-required', 'pending-subscription', 'presence-subscription-required', 'subid-required', 'too-many-subscriptions', 'unsupported'} condition_ns = 'http://jabber.org/protocol/pubsub#errors' def setup(self, xml): """Don't create XML for the plugin.""" self.xml = ET.Element('') def get_condition(self): """Return the condition element's name.""" for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: return cond return '' def set_condition(self, value): """ Set the tag name of the condition element. Arguments: value -- The tag name of the condition element. """ if value in self.conditions: del self['condition'] cond = ET.Element("{%s}%s" % (self.condition_ns, value)) self.parent().xml.append(cond) return self def del_condition(self): """Remove the condition element.""" for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: self.parent().xml.remove(child) return self def get_unsupported(self): """Return the name of an unsupported feature""" xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns) if xml is not None: return xml.attrib.get('feature', '') return '' def set_unsupported(self, value): """Mark a feature as unsupported""" self.del_unsupported() xml = ET.Element('{%s}unsupported' % self.condition_ns) xml.attrib['feature'] = value self.parent().xml.append(xml) def del_unsupported(self): """Delete an unsupported feature condition.""" xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns) if xml is not None: self.parent().xml.remove(xml) register_stanza_plugin(Error, PubsubErrorCondition) slixmpp/slixmpp/plugins/xep_0060/stanza/pubsub_event.py000066400000000000000000000101601477105560000235460ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import datetime as dt from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins import xep_0082 class Event(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'event' plugin_attrib = 'pubsub_event' interfaces = set() class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'item' plugin_attrib = name interfaces = {'id', 'payload', 'node', 'publisher'} def set_payload(self, value): self.xml.append(value) def get_payload(self): children = list(self.xml) if len(children) > 0: return children[0] def del_payload(self): for child in self.xml: self.xml.remove(child) class EventRetract(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'retract' plugin_attrib = name interfaces = {'id'} class EventItems(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'items' plugin_attrib = name interfaces = {'node'} class EventCollection(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'collection' plugin_attrib = name interfaces = {'node'} class EventAssociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'associate' plugin_attrib = name interfaces = {'node'} class EventDisassociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'disassociate' plugin_attrib = name interfaces = {'node'} class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'configuration' plugin_attrib = name interfaces = {'node'} class EventPurge(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'purge' plugin_attrib = name interfaces = {'node'} class EventDelete(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'delete' plugin_attrib = name interfaces = {'node', 'redirect'} def set_redirect(self, uri): del self['redirect'] redirect = ET.Element('{%s}redirect' % self.namespace) redirect.attrib['uri'] = uri self.xml.append(redirect) def get_redirect(self): redirect = self.xml.find('{%s}redirect' % self.namespace) if redirect is not None: return redirect.attrib.get('uri', '') return '' def del_redirect(self): redirect = self.xml.find('{%s}redirect' % self.namespace) if redirect is not None: self.xml.remove(redirect) class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'subscription' plugin_attrib = name interfaces = {'node', 'expiry', 'jid', 'subid', 'subscription'} def get_expiry(self): expiry = self._get_attr('expiry') if expiry.lower() == 'presence': return expiry return xep_0082.parse(expiry) def set_expiry(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('expiry', value) def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Message, Event) register_stanza_plugin(Event, EventCollection) register_stanza_plugin(Event, EventConfiguration) register_stanza_plugin(Event, EventPurge) register_stanza_plugin(Event, EventDelete) register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventSubscription) register_stanza_plugin(EventCollection, EventAssociate) register_stanza_plugin(EventCollection, EventDisassociate) register_stanza_plugin(EventConfiguration, Form) register_stanza_plugin(EventItems, EventItem, iterable=True) register_stanza_plugin(EventItems, EventRetract, iterable=True) slixmpp/slixmpp/plugins/xep_0060/stanza/pubsub_owner.py000066400000000000000000000075171477105560000235730ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp import Iq from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting from slixmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation from slixmpp.plugins.xep_0060.stanza.pubsub import Configure, Subscriptions class PubsubOwner(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'pubsub' plugin_attrib = 'pubsub_owner' interfaces = set(tuple()) class DefaultConfig(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'default' plugin_attrib = name interfaces = {'node', 'config'} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def get_config(self): return self['form'] def set_config(self, value): del self['from'] self.append(value) return self class OwnerAffiliations(Affiliations): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'node'} def append(self, affiliation): if not isinstance(affiliation, OwnerAffiliation): raise TypeError self.xml.append(affiliation.xml) class OwnerAffiliation(Affiliation): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'affiliation', 'jid'} class OwnerConfigure(Configure): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'configure' plugin_attrib = name interfaces = {'node'} class OwnerDefault(OwnerConfigure): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = {'node'} class OwnerDelete(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'delete' plugin_attrib = name interfaces = {'node'} class OwnerPurge(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'purge' plugin_attrib = name interfaces = {'node'} class OwnerRedirect(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'redirect' plugin_attrib = name interfaces = {'node', 'jid'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class OwnerSubscriptions(Subscriptions): name = 'subscriptions' namespace = 'http://jabber.org/protocol/pubsub#owner' plugin_attrib = name interfaces = {'node'} def append(self, subscription): if not isinstance(subscription, OwnerSubscription): raise TypeError self.xml.append(subscription.xml) class OwnerSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'subscription' plugin_attrib = name interfaces = {'jid', 'subscription'} def set_jid(self, value): self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Iq, PubsubOwner) register_stanza_plugin(PubsubOwner, DefaultConfig) register_stanza_plugin(PubsubOwner, OwnerAffiliations) register_stanza_plugin(PubsubOwner, OwnerConfigure) register_stanza_plugin(PubsubOwner, OwnerDefault) register_stanza_plugin(PubsubOwner, OwnerDelete) register_stanza_plugin(PubsubOwner, OwnerPurge) register_stanza_plugin(PubsubOwner, OwnerSubscriptions) register_stanza_plugin(DefaultConfig, Form) register_stanza_plugin(OwnerAffiliations, OwnerAffiliation, iterable=True) register_stanza_plugin(OwnerConfigure, Form) register_stanza_plugin(OwnerDefault, Form) register_stanza_plugin(OwnerDelete, OwnerRedirect) register_stanza_plugin(OwnerSubscriptions, OwnerSubscription, iterable=True) slixmpp/slixmpp/plugins/xep_0065/000077500000000000000000000000001477105560000172025ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0065/__init__.py000066400000000000000000000003601477105560000213120ustar00rootroot00000000000000from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0065.socks5 import Socks5Protocol from slixmpp.plugins.xep_0065.stanza import Socks5 from slixmpp.plugins.xep_0065.proxy import XEP_0065 register_plugin(XEP_0065) slixmpp/slixmpp/plugins/xep_0065/proxy.py000066400000000000000000000241111477105560000207340ustar00rootroot00000000000000import asyncio import logging import socket from hashlib import sha1 from uuid import uuid4 from slixmpp.stanza import Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0065 import stanza, Socks5, Socks5Protocol log = logging.getLogger(__name__) class XEP_0065(BasePlugin): name = 'xep_0065' description = "XEP-0065: SOCKS5 Bytestreams" dependencies = {'xep_0030'} default_config = { 'auto_accept': False } def plugin_init(self): register_stanza_plugin(Iq, Socks5) self._proxies = {} self._sessions = {} self._preauthed_sids = {} self.xmpp.register_handler(CoroutineCallback( 'Socks5 Bytestreams', StanzaPath('iq@type=set/socks/streamhost'), self._handle_streamhost )) self.api.register(self._authorized, 'authorized', default=True) self.api.register(self._authorized_sid, 'authorized_sid', default=True) self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Socks5.namespace) def plugin_end(self): self.xmpp.remove_handler('Socks5 Bytestreams') self.xmpp.remove_handler('Socks5 Streamhost Used') self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace) def get_socket(self, sid): """Returns the socket associated to the SID.""" return self._sessions.get(sid, None) async def handshake(self, to, ifrom=None, sid=None, timeout=None): """ Starts the handshake to establish the socks5 bytestreams connection. """ if not self._proxies: self._proxies = await self.discover_proxies() if sid is None: sid = uuid4().hex used = await self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) proxy = used['socks']['streamhost_used']['jid'] if proxy not in self._proxies: log.warning('Received unknown SOCKS5 proxy: %s', proxy) return try: self._sessions[sid] = (await self._connect_proxy( self._get_dest_sha1(sid, self.xmpp.boundjid, to), self._proxies[proxy][0], self._proxies[proxy][1]))[1] except socket.error: return None addr, port = await self._sessions[sid].connected # Request that the proxy activate the session with the target. await self.activate(proxy, sid, to, timeout=timeout) sock = self.get_socket(sid) self.xmpp.event('stream:%s:%s' % (sid, to), sock) return sock def request_stream(self, to, sid=None, ifrom=None, timeout=None, callback=None): if sid is None: sid = uuid4().hex # Requester initiates S5B negotiation with Target by sending # IQ-set that includes the JabberID and network address of # StreamHost as well as the StreamID (SID) of the proposed # bytestream. iq = self.xmpp.Iq() iq['to'] = to iq['from'] = ifrom iq['type'] = 'set' iq['socks']['sid'] = sid for proxy, (host, port) in self._proxies.items(): iq['socks'].add_streamhost(proxy, host, port) return iq.send(timeout=timeout, callback=callback) async def discover_proxies(self, jid=None, ifrom=None, timeout=None): """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" if jid is None: if self.xmpp.is_component: jid = self.xmpp.server else: jid = self.xmpp.boundjid.server discovered = set() disco_items = await self.xmpp['xep_0030'].get_items(jid, timeout=timeout) disco_items = {item[0] for item in disco_items['disco_items']['items']} disco_info_futures = {} for item in disco_items: disco_info_futures[item] = self.xmpp['xep_0030'].get_info(item, timeout=timeout) for item in disco_items: try: disco_info = await disco_info_futures[item] except XMPPError: continue else: # Verify that the identity is a bytestream proxy. identities = disco_info['disco_info']['identities'] for identity in identities: if identity[0] == 'proxy' and identity[1] == 'bytestreams': discovered.add(disco_info['from']) for jid in discovered: try: addr = await self.get_network_address(jid, ifrom=ifrom, timeout=timeout) self._proxies[jid] = (addr['socks']['streamhost']['host'], addr['socks']['streamhost']['port']) except XMPPError: continue return self._proxies def get_network_address(self, proxy, ifrom=None, timeout=None, callback=None): """Get the network information of a proxy.""" iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom) iq.enable('socks') return iq.send(timeout=timeout, callback=callback) def _get_dest_sha1(self, sid, requester, target): # The hostname MUST be SHA1(SID + Requester JID + Target JID) # where the output is hexadecimal-encoded (not binary). digest = sha1() digest.update(sid.encode('utf8')) digest.update(str(requester).encode('utf8')) digest.update(str(target).encode('utf8')) return digest.hexdigest() async def _handle_streamhost(self, iq): """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] if not sid: raise XMPPError(etype='modify', condition='bad-request') if not await self._accept_stream(iq): raise XMPPError(etype='modify', condition='not-acceptable') streamhosts = iq['socks']['streamhosts'] requester = iq['from'] target = iq['to'] dest = self._get_dest_sha1(sid, requester, target) proxy_futures = [] for streamhost in streamhosts: proxy_futures.append(self._connect_proxy( dest, streamhost['host'], streamhost['port'])) proxies = await asyncio.gather(*proxy_futures, return_exceptions=True) for streamhost, proxy in zip(streamhosts, proxies): if isinstance(proxy, ValueError): continue elif isinstance(proxy, socket.error): log.error('Socket error while connecting to the proxy.') continue proxy = proxy[1] # TODO: what if the future never happens? try: addr, port = await proxy.connected except socket.error: log.exception('Socket error while connecting to the proxy.') continue # TODO: make a better choice than just the first working one. used_streamhost = streamhost['jid'] conn = proxy break else: raise XMPPError(etype='cancel', condition='item-not-found') # TODO: close properly the connection to the other proxies. iq = iq.reply() self._sessions[sid] = conn iq['socks']['sid'] = sid iq['socks']['streamhost_used']['jid'] = used_streamhost iq.send() self.xmpp.event('socks5_stream', conn) self.xmpp.event('stream:%s:%s' % (sid, requester), conn) def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None): """Activate the socks5 session that has been negotiated.""" iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom) iq['socks']['sid'] = sid iq['socks']['activate'] = target return iq.send(timeout=timeout, callback=callback) def deactivate(self, sid): """Closes the proxy socket associated with this SID.""" sock = self._sessions.get(sid) if sock: try: # sock.close() will also delete sid from self._sessions (see _connect_proxy) sock.close() except socket.error: pass # Though this should not be necessary remove the closed session anyway if sid in self._sessions: log.warn(('SOCKS5 session with sid = "%s" was not ' + 'removed from _sessions by sock.close()') % sid) del self._sessions[sid] def close(self): """Closes all proxy sockets.""" for sid, sock in self._sessions.items(): sock.close() self._sessions = {} def _connect_proxy(self, dest, proxy, proxy_port): """ Returns a future to a connection between the client and the server-side Socks5 proxy. dest : The SHA-1 of (SID + Requester JID + Target JID), in hex. host : The hostname or the IP of the proxy. port : The port of the proxy. or """ factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event) return self.xmpp.loop.create_connection(factory, proxy, proxy_port) async def _accept_stream(self, iq): receiver = iq['to'] sender = iq['from'] sid = iq['socks']['sid'] if await self.api['authorized_sid'](receiver, sid, sender, iq): return True return await self.api['authorized'](receiver, sid, sender, iq) def _authorized(self, jid, sid, ifrom, iq): return self.auto_accept def _authorized_sid(self, jid, sid, ifrom, iq): log.debug('>>> authed sids: %s', self._preauthed_sids) log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) if (jid, sid, ifrom) in self._preauthed_sids: del self._preauthed_sids[(jid, sid, ifrom)] return True return False def _preauthorize_sid(self, jid, sid, ifrom, data): log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) self._preauthed_sids[(jid, sid, ifrom)] = True slixmpp/slixmpp/plugins/xep_0065/socks5.py000066400000000000000000000167661477105560000210030ustar00rootroot00000000000000'''Pure asyncio implementation of RFC 1928 - SOCKS Protocol Version 5.''' import asyncio import enum import logging import socket import struct from slixmpp.stringprep import punycode, StringprepError log = logging.getLogger(__name__) class ProtocolMismatch(Exception): '''We only implement SOCKS5, no other version or protocol.''' class ProtocolError(Exception): '''Some protocol error.''' class MethodMismatch(Exception): '''The server answered with a method we didn’t ask for.''' class MethodUnacceptable(Exception): '''None of our methods is supported by the server.''' class AddressTypeUnacceptable(Exception): '''The address type (ATYP) field isn’t one of IPv4, IPv6 or domain name.''' class ReplyError(Exception): '''The server answered with an error.''' possible_values = ( "succeeded", "general SOCKS server failure", "connection not allowed by ruleset", "Network unreachable", "Host unreachable", "Connection refused", "TTL expired", "Command not supported", "Address type not supported", "Unknown error") def __init__(self, result): if result < 9: Exception.__init__(self, self.possible_values[result]) else: Exception.__init__(self, self.possible_values[9]) class Method(enum.IntEnum): '''Known methods for a SOCKS5 session.''' none = 0 gssapi = 1 password = 2 # Methods 3 to 127 are reserved by IANA. # Methods 128 to 254 are reserved for private use. unacceptable = 255 not_yet_selected = -1 class Command(enum.IntEnum): '''Existing commands for requests.''' connect = 1 bind = 2 udp_associate = 3 class AddressType(enum.IntEnum): '''Existing address types.''' ipv4 = 1 domain = 3 ipv6 = 4 class Socks5Protocol(asyncio.Protocol): '''This implements SOCKS5 as an asyncio protocol.''' def __init__(self, dest_addr, dest_port, event): self.methods = {Method.none} self.selected_method = Method.not_yet_selected self.transport = None self.dest = (dest_addr, dest_port) self.connected = asyncio.Future() self.event = event self.paused = asyncio.Future() self.paused.set_result(None) def register_method(self, method): '''Register a SOCKS5 method.''' self.methods.add(method) def unregister_method(self, method): '''Unregister a SOCKS5 method.''' self.methods.remove(method) def connection_made(self, transport): '''Called when the connection to the SOCKS5 server is established.''' log.debug('SOCKS5 connection established.') self.transport = transport self._send_methods() def data_received(self, data): '''Called when we received some data from the SOCKS5 server.''' log.debug('SOCKS5 message received.') # If we are already connected, this is a data packet. if self.connected.done(): return self.event('socks5_data', data) # Every SOCKS5 message starts with the protocol version. if data[0] != 5: raise ProtocolMismatch() # Then select the correct handler for the data we just received. if self.selected_method == Method.not_yet_selected: self._handle_method(data) else: self._handle_connect(data) def connection_lost(self, exc): log.debug('SOCKS5 connection closed.') self.event('socks5_closed', exc) def pause_writing(self): self.paused = asyncio.Future() def resume_writing(self): self.paused.set_result(None) async def write(self, data): await self.paused self.transport.write(data) def _send_methods(self): '''Send the methods request, first thing a client should do.''' # Create the buffer for our request. request = bytearray(len(self.methods) + 2) # Protocol version. request[0] = 5 # Number of methods to send. request[1] = len(self.methods) # List every method we support. for i, method in enumerate(self.methods): request[i + 2] = method # Send the request. self.transport.write(request) def _send_request(self, command): '''Send a request, should be done after having negociated a method.''' # Encode the destination address to embed it in our request. # We need to do that first because its length is variable. address, port = self.dest addr = self._encode_addr(address) # Create the buffer for our request. request = bytearray(5 + len(addr)) # Protocol version. request[0] = 5 # Specify the command we want to use. request[1] = command # request[2] is reserved, keeping it at 0. # Add our destination address and port. request[3:3+len(addr)] = addr request[-2:] = struct.pack('>H', port) # Send the request. log.debug('SOCKS5 message sent.') self.transport.write(request) def _handle_method(self, data): '''Handle a method reply from the server.''' if len(data) != 2: raise ProtocolError() selected_method = data[1] if selected_method not in self.methods: raise MethodMismatch() if selected_method == Method.unacceptable: raise MethodUnacceptable() self.selected_method = selected_method self._send_request(Command.connect) def _handle_connect(self, data): '''Handle a connect reply from the server.''' try: addr, port = self._parse_result(data) except ReplyError as exception: self.connected.set_exception(exception) self.connected.set_result((addr, port)) self.event('socks5_connected', (addr, port)) def _parse_result(self, data): '''Parse a reply from the server.''' result = data[1] if result != 0: raise ReplyError(result) addr = self._parse_addr(data[3:-2]) port = struct.unpack('>H', data[-2:])[0] return (addr, port) @staticmethod def _parse_addr(addr): '''Parse an address (IP or domain) from a bytestream.''' addr_type = addr[0] if addr_type == AddressType.ipv6: try: return socket.inet_ntop(socket.AF_INET6, addr[1:]) except ValueError as e: raise AddressTypeUnacceptable(e) if addr_type == AddressType.ipv4: try: return socket.inet_ntop(socket.AF_INET, addr[1:]) except ValueError as e: raise AddressTypeUnacceptable(e) if addr_type == AddressType.domain: length = addr[1] address = addr[2:] if length != len(address): raise Exception('Size mismatch') return address.decode() raise AddressTypeUnacceptable(addr_type) @staticmethod def _encode_addr(addr): '''Encode an address (IP or domain) into a bytestream.''' try: ipv6 = socket.inet_pton(socket.AF_INET6, addr) return b'\x04' + ipv6 except OSError: pass try: ipv4 = socket.inet_aton(addr) return b'\x01' + ipv4 except OSError: pass try: domain = punycode(addr) return b'\x03' + bytes([len(domain)]) + domain except StringprepError: pass raise Exception('Err…') slixmpp/slixmpp/plugins/xep_0065/stanza.py000066400000000000000000000023701477105560000210560ustar00rootroot00000000000000from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Socks5(ElementBase): name = 'query' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'socks' interfaces = {'sid', 'activate'} sub_interfaces = {'activate'} def add_streamhost(self, jid, host, port): sh = StreamHost(parent=self) sh['jid'] = jid sh['host'] = host sh['port'] = port class StreamHost(ElementBase): name = 'streamhost' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost' plugin_multi_attrib = 'streamhosts' interfaces = {'host', 'jid', 'port'} def set_jid(self, value): return self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) class StreamHostUsed(ElementBase): name = 'streamhost-used' namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost_used' interfaces = {'jid'} def set_jid(self, value): return self._set_attr('jid', str(value)) def get_jid(self): return JID(self._get_attr('jid')) register_stanza_plugin(Socks5, StreamHost, iterable=True) register_stanza_plugin(Socks5, StreamHostUsed) slixmpp/slixmpp/plugins/xep_0066/000077500000000000000000000000001477105560000172035ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0066/__init__.py000066400000000000000000000006251477105560000213170ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0066 import stanza from slixmpp.plugins.xep_0066.stanza import OOB, OOBTransfer from slixmpp.plugins.xep_0066.oob import XEP_0066 register_plugin(XEP_0066) slixmpp/slixmpp/plugins/xep_0066/oob.py000066400000000000000000000113221477105560000203330ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.stanza import Message, Presence, Iq from slixmpp.exceptions import XMPPError from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0066 import stanza log = logging.getLogger(__name__) class XEP_0066(BasePlugin): """ XEP-0066: Out of Band Data Out of Band Data is a basic method for transferring files between XMPP agents. The URL of the resource in question is sent to the receiving entity, which then downloads the resource before responding to the OOB request. OOB is also used as a generic means to transmit URLs in other stanzas to indicate where to find additional information. Also see . Events: oob_transfer -- Raised when a request to download a resource has been received. """ name = 'xep_0066' description = 'XEP-0066: Out of Band Data' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): """Start the XEP-0066 plugin.""" self.url_handlers = {'global': self._default_handler, 'jid': {}} register_stanza_plugin(Iq, stanza.OOBTransfer) register_stanza_plugin(Message, stanza.OOB) register_stanza_plugin(Presence, stanza.OOB) self.xmpp.register_handler( Callback('OOB Transfer', StanzaPath('iq@type=set/oob_transfer'), self._handle_transfer)) def plugin_end(self): self.xmpp.remove_handler('OOB Transfer') self.xmpp['xep_0030'].del_feature(feature=stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].del_feature(feature=stanza.OOB.namespace) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) def register_url_handler(self, jid=None, handler=None): """ Register a handler to process download requests, either for all JIDs or a single JID. :param jid: If None, then set the handler as a global default. :param handler: If None, then remove the existing handler for the given JID, or reset the global handler if the JID is None. """ if jid is None: if handler is not None: self.url_handlers['global'] = handler else: self.url_handlers['global'] = self._default_handler else: if handler is not None: self.url_handlers['jid'][jid] = handler else: del self.url_handlers['jid'][jid] def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): """ Initiate a basic file transfer by sending the URL of a file or other resource. :param url: The URL of the resource to transfer. :param desc: An optional human readable description of the item that is to be transferred. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = to iq['from'] = ifrom iq['oob_transfer']['url'] = url iq['oob_transfer']['desc'] = desc return iq.send(**iqargs) def _run_url_handler(self, iq): """ Execute the appropriate handler for a transfer request. :param iq: The Iq stanza containing the OOB transfer request. """ if iq['to'] in self.url_handlers['jid']: return self.url_handlers['jid'][iq['to']](iq) else: if self.url_handlers['global']: self.url_handlers['global'](iq) else: raise XMPPError('service-unavailable') def _default_handler(self, iq): """ As a safe default, don't actually download files. Register a new handler using self.register_url_handler to screen requests and download files. :param iq: The Iq stanza containing the OOB transfer request. """ raise XMPPError('service-unavailable') def _handle_transfer(self, iq): """ Handle receiving an out-of-band transfer request. :param iq: An Iq stanza containing an OOB transfer request. """ log.debug('Received out-of-band data request for %s from %s:' % ( iq['oob_transfer']['url'], iq['from'])) self._run_url_handler(iq) iq.reply().send() slixmpp/slixmpp/plugins/xep_0066/stanza.py000066400000000000000000000011421477105560000210530ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class OOBTransfer(ElementBase): """ """ name = 'query' namespace = 'jabber:iq:oob' plugin_attrib = 'oob_transfer' interfaces = {'url', 'desc', 'sid'} sub_interfaces = {'url', 'desc'} class OOB(ElementBase): """ """ name = 'x' namespace = 'jabber:x:oob' plugin_attrib = 'oob' interfaces = {'url', 'desc'} sub_interfaces = interfaces slixmpp/slixmpp/plugins/xep_0070/000077500000000000000000000000001477105560000171765ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0070/__init__.py000066400000000000000000000005231477105560000213070ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0070.stanza import Confirm from slixmpp.plugins.xep_0070.confirm import XEP_0070 register_plugin(XEP_0070) slixmpp/slixmpp/plugins/xep_0070/confirm.py000066400000000000000000000050151477105560000212060ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from uuid import uuid4 from slixmpp.plugins import BasePlugin from slixmpp import Iq, Message from slixmpp.jid import JID from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins.xep_0070 import stanza, Confirm log = logging.getLogger(__name__) class XEP_0070(BasePlugin): """ XEP-0070 Verifying HTTP Requests via XMPP """ name = 'xep_0070' description = 'XEP-0070: Verifying HTTP Requests via XMPP' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, Confirm) register_stanza_plugin(Message, Confirm) self.xmpp.register_handler( Callback('Confirm', StanzaPath('iq@type=get/confirm'), self._handle_iq_confirm)) self.xmpp.register_handler( Callback('Confirm', StanzaPath('message/confirm'), self._handle_message_confirm)) def plugin_end(self): self.xmpp.remove_handler('Confirm') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/http-auth') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/http-auth') def ask_confirm(self, jid, id, url, method, *, ifrom=None, message=None): jid = JID(jid) if jid.resource: stanza = self.xmpp.Iq() stanza['type'] = 'get' else: stanza = self.xmpp.Message() stanza['thread'] = uuid4().hex stanza['from'] = ifrom stanza['to'] = jid stanza['confirm']['id'] = id stanza['confirm']['url'] = url stanza['confirm']['method'] = method if not jid.resource: if message is not None: stanza['body'] = message.format(id=id, url=url, method=method) stanza.send() fut = asyncio.Future() fut.set_result(stanza) return fut else: return stanza.send() def _handle_iq_confirm(self, iq): self.xmpp.event('http_confirm_iq', iq) self.xmpp.event('http_confirm', iq) def _handle_message_confirm(self, message): self.xmpp.event('http_confirm_message', message) self.xmpp.event('http_confirm', message) slixmpp/slixmpp/plugins/xep_0070/stanza.py000066400000000000000000000005671477105560000210600ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2015 Emmanuel Gil Peyrot # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class Confirm(ElementBase): name = 'confirm' namespace = 'http://jabber.org/protocol/http-auth' plugin_attrib = 'confirm' interfaces = {'id', 'url', 'method'} slixmpp/slixmpp/plugins/xep_0071/000077500000000000000000000000001477105560000171775ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0071/__init__.py000066400000000000000000000005441477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0071.stanza import XHTML_IM from slixmpp.plugins.xep_0071.xhtml_im import XEP_0071 register_plugin(XEP_0071) slixmpp/slixmpp/plugins/xep_0071/stanza.py000066400000000000000000000053211477105560000210520ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Message from slixmpp.util import unicode from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring XHTML_NS = 'http://www.w3.org/1999/xhtml' class XHTML_IM(ElementBase): namespace = 'http://jabber.org/protocol/xhtml-im' name = 'html' interfaces = {'body'} lang_interfaces = {'body'} plugin_attrib = name def set_body(self, content, lang=None): if lang is None: lang = self.get_lang() self.del_body(lang) if lang == '*': for sublang, subcontent in content.items(): self.set_body(subcontent, sublang) else: if isinstance(content, type(ET.Element('test'))): content = unicode(ET.tostring(content)) else: content = unicode(content) header = ' in case the requesting entity is already registered to us user_validate((self, jid, node, ifrom, registration) Add the user to the user store or raise ValueError(msg) if any problem is encountered msg is sent back to the XMPP client as an error message. """ name = 'xep_0077' description = 'XEP-0077: In-Band Registration' dependencies = {'xep_0004', 'xep_0066'} stanza = stanza default_config = { 'create_account': True, 'force_registration': False, 'order': 50, "form_fields": {"username", "password"}, "form_instructions": "Enter your credentials", } def plugin_init(self): register_stanza_plugin(StreamFeatures, RegisterFeature) register_stanza_plugin(Iq, Register) if self.xmpp.is_component: self.xmpp["xep_0030"].add_feature("jabber:iq:register") self.xmpp.register_handler( CoroutineCallback( "registration", StanzaPath("/iq/register"), self._handle_registration, ) ) self._user_store = {} self.api.register(self._user_get, "user_get") self.api.register(self._user_remove, "user_remove") self.api.register(self._make_registration_form, "make_registration_form") self.api.register(self._user_validate, "user_validate") else: self.xmpp.register_feature( "register", self._handle_register_feature, restart=False, order=self.order, ) register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form) register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB) self.xmpp.add_event_handler('connected', self._force_registration) def plugin_end(self): if not self.xmpp.is_component: self.xmpp.unregister_feature('register', self.order) def _user_get(self, jid, node, ifrom, iq): return self._user_store.get(iq["from"].bare) def _user_remove(self, jid, node, ifrom, iq): return self._user_store.pop(iq["from"].bare) async def _make_registration_form(self, jid, node, ifrom, iq: Iq): reg = iq["register"] user = await self.api["user_get"](None, None, iq['from'], iq) if user is None: user = {} else: reg["registered"] = True reg["instructions"] = self.form_instructions for field in self.form_fields: data = user.get(field, "") if data: reg[field] = data else: # Add a blank field reg.add_field(field) reply = iq.reply() reply.set_payload(reg.xml) return reply def _user_validate(self, jid, node, ifrom, registration): self._user_store[ifrom.bare] = {key: registration[key] for key in self.form_fields} async def _handle_registration(self, iq: Iq): if iq["type"] == "get": await self._send_form(iq) elif iq["type"] == "set": if iq["register"]["remove"]: try: await self.api["user_remove"](None, None, iq["from"], iq) except KeyError: _send_error( iq, "404", "cancel", "item-not-found", "User not found", ) else: reply = iq.reply() reply.send() self.xmpp.event("user_unregister", iq) return for field in self.form_fields: if not iq["register"][field]: # Incomplete Registration _send_error( iq, "406", "modify", "not-acceptable", "Please fill in all fields.", ) return try: await self.api["user_validate"](None, None, iq["from"], iq["register"]) except ValueError as e: _send_error( iq, "406", "modify", "not-acceptable", e.args, ) else: reply = iq.reply() reply.send() self.xmpp.event("user_register", iq) async def _send_form(self, iq): reply = await self.api["make_registration_form"](None, None, iq["from"], iq) reply.send() def _force_registration(self, event): if self.force_registration: self.xmpp.add_filter('in', self._force_stream_feature) def _force_stream_feature(self, stanza): if isinstance(stanza, StreamFeatures): if self.xmpp.enable_starttls: if 'starttls' not in self.xmpp.features: return stanza elif not isinstance(self.xmpp.socket, ssl.SSLSocket): return stanza if 'mechanisms' not in self.xmpp.features: log.debug('Forced adding in-band registration stream feature') stanza.enable('register') self.xmpp.del_filter('in', self._force_stream_feature) return stanza async def _handle_register_feature(self, features): if 'mechanisms' in self.xmpp.features: # We have already logged in with an account return False if self.create_account and self.xmpp.event_handled('register'): form = await self.get_registration() await self.xmpp.event_async('register', form) return True return False def get_registration(self, jid=None, ifrom=None, timeout=None, callback=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid iq['from'] = ifrom iq.enable('register') return iq.send(timeout=timeout, callback=callback) def cancel_registration(self, jid=None, ifrom=None, timeout=None, callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom iq['register']['remove'] = True return iq.send(timeout=timeout, callback=callback) def change_password(self, password, jid=None, ifrom=None, timeout=None, callback=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid iq['from'] = ifrom if self.xmpp.is_component: ifrom = JID(ifrom) iq['register']['username'] = ifrom.user else: iq['register']['username'] = self.xmpp.boundjid.user iq['register']['password'] = password return iq.send(timeout=timeout, callback=callback) def _send_error(iq, code, error_type, name, text=""): # It would be nice to raise XMPPError but the iq payload # should include the register info reply = iq.reply() reply.set_payload(iq["register"].xml) reply.error() reply["error"]["code"] = code reply["error"]["type"] = error_type reply["error"]["condition"] = name reply["error"]["text"] = text reply.send() slixmpp/slixmpp/plugins/xep_0077/stanza.py000066400000000000000000000041311477105560000210560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import unicode_literals from slixmpp.xmlstream import ElementBase, ET class Register(ElementBase): namespace = 'jabber:iq:register' name = 'query' plugin_attrib = 'register' interfaces = {'username', 'password', 'email', 'nick', 'name', 'first', 'last', 'address', 'city', 'state', 'zip', 'phone', 'url', 'date', 'misc', 'text', 'key', 'registered', 'remove', 'instructions', 'fields'} sub_interfaces = interfaces form_fields = {'username', 'password', 'email', 'nick', 'name', 'first', 'last', 'address', 'city', 'state', 'zip', 'phone', 'url', 'date', 'misc', 'text', 'key'} def get_registered(self): present = self.xml.find('{%s}registered' % self.namespace) return present is not None def get_remove(self): present = self.xml.find('{%s}remove' % self.namespace) return present is not None def set_registered(self, value): if value: self.add_field('registered') else: del self['registered'] def set_remove(self, value): if value: self.add_field('remove') else: del self['remove'] def add_field(self, value): self._set_sub_text(value, '', keep=True) def get_fields(self): fields = set() for field in self.form_fields: if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: fields.add(field) return fields def set_fields(self, fields): del self['fields'] for field in fields: self._set_sub_text(field, '', keep=True) def del_fields(self): for field in self.form_fields: self._del_sub(field) class RegisterFeature(ElementBase): name = 'register' namespace = 'http://jabber.org/features/iq-register' plugin_attrib = name interfaces = set() slixmpp/slixmpp/plugins/xep_0078/000077500000000000000000000000001477105560000172065ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0078/__init__.py000066400000000000000000000006371477105560000213250ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0078 import stanza from slixmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature from slixmpp.plugins.xep_0078.legacyauth import XEP_0078 register_plugin(XEP_0078) slixmpp/slixmpp/plugins/xep_0078/legacyauth.py000066400000000000000000000103021477105560000217020ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import uuid import logging import hashlib from slixmpp.jid import JID from slixmpp.exceptions import IqError, IqTimeout from slixmpp.stanza import Iq, StreamFeatures from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0078 import stanza log = logging.getLogger(__name__) class XEP_0078(BasePlugin): """ XEP-0078 NON-SASL Authentication This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin unless you are forced to use an old XMPP server implementation. """ name = 'xep_0078' description = 'XEP-0078: Non-SASL Authentication' dependencies = set() stanza = stanza default_config = { 'order': 15 } def plugin_init(self): self.xmpp.register_feature('auth', self._handle_auth, restart=False, order=self.order) self.xmpp.add_event_handler('legacy_protocol', self._handle_legacy_protocol) register_stanza_plugin(Iq, stanza.IqAuth) register_stanza_plugin(StreamFeatures, stanza.AuthFeature) def plugin_end(self): self.xmpp.del_event_handler('legacy_protocol', self._handle_legacy_protocol) self.xmpp.unregister_feature('auth', self.order) def _handle_auth(self, features): # If we can or have already authenticated with SASL, do nothing. if 'mechanisms' in features['features']: return False return self.authenticate() def _handle_legacy_protocol(self, event): self.authenticate() def authenticate(self): if self.xmpp.authenticated: return False log.debug("Starting jabber:iq:auth Authentication") # Step 1: Request the auth form iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = self.xmpp.requested_jid.host iq['auth']['username'] = self.xmpp.requested_jid.user try: resp = iq.send() except IqError as err: log.info("Authentication failed: %s", err.iq['error']['condition']) self.xmpp.event('failed_auth') self.xmpp.disconnect() return True except IqTimeout: log.info("Authentication failed: %s", 'timeout') self.xmpp.event('failed_auth') self.xmpp.disconnect() return True # Step 2: Fill out auth form for either password or digest auth iq = self.xmpp.Iq() iq['type'] = 'set' iq['auth']['username'] = self.xmpp.requested_jid.user # A resource is required, so create a random one if necessary resource = self.xmpp.requested_jid.resource if not resource: resource = str(uuid.uuid4()) iq['auth']['resource'] = resource if 'digest' in resp['auth']['fields']: log.debug('Authenticating via jabber:iq:auth Digest') stream_id = bytes(self.xmpp.stream_id, encoding='utf-8') password = bytes(self.xmpp.password, encoding='utf-8') digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest() iq['auth']['digest'] = digest else: log.warning('Authenticating via jabber:iq:auth Plain.') iq['auth']['password'] = self.xmpp.password # Step 3: Send credentials try: result = iq.send() except IqError as err: log.info("Authentication failed") self.xmpp.event("failed_auth") self.xmpp.disconnect() except IqTimeout: log.info("Authentication failed") self.xmpp.event("failed_auth") self.xmpp.disconnect() self.xmpp.features.add('auth') self.xmpp.authenticated = True self.xmpp.boundjid = JID(self.xmpp.requested_jid) self.xmpp.boundjid.resource = resource self.xmpp.event('session_bind', self.xmpp.boundjid) log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.event('session_start') return True slixmpp/slixmpp/plugins/xep_0078/stanza.py000066400000000000000000000021671477105560000210660ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class IqAuth(ElementBase): namespace = 'jabber:iq:auth' name = 'query' plugin_attrib = 'auth' interfaces = {'fields', 'username', 'password', 'resource', 'digest'} sub_interfaces = {'username', 'password', 'resource', 'digest'} plugin_tag_map = {} plugin_attrib_map = {} def get_fields(self): fields = set() for field in self.sub_interfaces: if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: fields.add(field) return fields def set_resource(self, value): self._set_sub_text('resource', value, keep=True) def set_password(self, value): self._set_sub_text('password', value, keep=True) class AuthFeature(ElementBase): namespace = 'http://jabber.org/features/iq-auth' name = 'auth' plugin_attrib = 'auth' interfaces = set() plugin_tag_map = {} plugin_attrib_map = {} slixmpp/slixmpp/plugins/xep_0079/000077500000000000000000000000001477105560000172075ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0079/__init__.py000066400000000000000000000007331477105560000213230ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0079.stanza import ( AMP, Rule, InvalidRules, UnsupportedConditions, UnsupportedActions, FailedRules, FailedRule, AMPFeature) from slixmpp.plugins.xep_0079.amp import XEP_0079 register_plugin(XEP_0079) slixmpp/slixmpp/plugins/xep_0079/amp.py000066400000000000000000000047541477105560000203500ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio import logging from slixmpp.stanza import Message, Error, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.matcher import StanzaPath, MatchMany from slixmpp.xmlstream.handler import Callback from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0079 import stanza log = logging.getLogger(__name__) class XEP_0079(BasePlugin): """ XEP-0079 Advanced Message Processing """ name = 'xep_0079' description = 'XEP-0079: Advanced Message Processing' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, stanza.AMP) register_stanza_plugin(Error, stanza.InvalidRules) register_stanza_plugin(Error, stanza.UnsupportedConditions) register_stanza_plugin(Error, stanza.UnsupportedActions) register_stanza_plugin(Error, stanza.FailedRules) self.xmpp.register_handler( Callback('AMP Response', MatchMany([ StanzaPath('message/error/failed_rules'), StanzaPath('message/amp') ]), self._handle_amp_response)) if not self.xmpp.is_component: self.xmpp.register_feature('amp', self._handle_amp_feature, restart=False, order=9000) register_stanza_plugin(StreamFeatures, stanza.AMPFeature) def plugin_end(self): self.xmpp.remove_handler('AMP Response') def _handle_amp_response(self, msg): log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') if msg['type'] == 'error': self.xmpp.event('amp_error', msg) elif msg['amp']['status'] in ('alert', 'notify'): self.xmpp.event('amp_%s' % msg['amp']['status'], msg) def _handle_amp_feature(self, features): log.debug('Advanced Message Processing is available.') self.xmpp.features.add('amp') def discover_support(self, jid=None, **iqargs): if jid is None: if self.xmpp.is_component: jid = self.xmpp.server_host else: jid = self.xmpp.boundjid.host return self.xmpp['xep_0030'].get_info( jid=jid, node='http://jabber.org/protocol/amp', **iqargs) slixmpp/slixmpp/plugins/xep_0079/stanza.py000066400000000000000000000050351477105560000210640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import unicode_literals from slixmpp import JID from slixmpp.xmlstream import ElementBase, register_stanza_plugin class AMP(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'amp' plugin_attrib = 'amp' interfaces = {'from', 'to', 'status', 'per_hop'} def get_from(self): return JID(self._get_attr('from')) def set_from(self, value): return self._set_attr('from', str(value)) def get_to(self): return JID(self._get_attr('from')) def set_to(self, value): return self._set_attr('to', str(value)) def get_per_hop(self): return self._get_attr('per-hop') == 'true' def set_per_hop(self, value): if value: return self._set_attr('per-hop', 'true') else: return self._del_attr('per-hop') def del_per_hop(self): return self._del_attr('per-hop') def add_rule(self, action, condition, value): rule = Rule(parent=self) rule['action'] = action rule['condition'] = condition rule['value'] = value class Rule(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'rule' plugin_attrib = name plugin_multi_attrib = 'rules' interfaces = {'action', 'condition', 'value'} class InvalidRules(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'invalid-rules' plugin_attrib = 'invalid_rules' class UnsupportedConditions(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'unsupported-conditions' plugin_attrib = 'unsupported_conditions' class UnsupportedActions(ElementBase): namespace = 'http://jabber.org/protocol/amp' name = 'unsupported-actions' plugin_attrib = 'unsupported_actions' class FailedRule(Rule): namespace = 'http://jabber.org/protocol/amp#errors' class FailedRules(ElementBase): namespace = 'http://jabber.org/protocol/amp#errors' name = 'failed-rules' plugin_attrib = 'failed_rules' class AMPFeature(ElementBase): namespace = 'http://jabber.org/features/amp' name = 'amp' register_stanza_plugin(AMP, Rule, iterable=True) register_stanza_plugin(InvalidRules, Rule, iterable=True) register_stanza_plugin(UnsupportedConditions, Rule, iterable=True) register_stanza_plugin(UnsupportedActions, Rule, iterable=True) register_stanza_plugin(FailedRules, FailedRule, iterable=True) slixmpp/slixmpp/plugins/xep_0080/000077500000000000000000000000001477105560000171775ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0080/__init__.py000066400000000000000000000005511477105560000213110ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0080.stanza import Geoloc from slixmpp.plugins.xep_0080.geoloc import XEP_0080 register_plugin(XEP_0080) slixmpp/slixmpp/plugins/xep_0080/geoloc.py000066400000000000000000000076401477105560000210300ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional, Callable from slixmpp import JID from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0080 import stanza, Geoloc log = logging.getLogger(__name__) class XEP_0080(BasePlugin): """ XEP-0080: User Location """ name = 'xep_0080' description = 'XEP-0080: User Location' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0163'].remove_interest(Geoloc.namespace) self.xmpp['xep_0030'].del_feature(feature=Geoloc.namespace) def session_bind(self, jid: JID): self.xmpp['xep_0163'].register_pep('user_location', Geoloc) def publish_location(self, **kwargs) -> Future: """ Publish the user's current location. :param accuracy: Horizontal GPS error in meters. :param alt: Altitude in meters above or below sea level. :param area: A named area such as a campus or neighborhood. :param bearing: GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north. :param building: A specific building on a street or in an area. :param country: The nation where the user is located. :param countrycode: The ISO 3166 two-letter country code. :param datum: GPS datum. :param description: A natural-language name for or description of the location. :param error: Horizontal GPS error in arc minutes. Obsoleted by the accuracy parameter. :param floor: A particular floor in a building. :param lat: Latitude in decimal degrees North. :param locality: A locality within the administrative region, such as a town or city. :param lon: Longitude in decimal degrees East. :param postalcode: A code used for postal delivery. :param region: An administrative region of the nation, such as a state or province. :param room: A particular room in a building. :param speed: The speed at which the entity is moving, in meters per second. :param street: A thoroughfare within the locality, or a crossing of two thoroughfares. :param text: A catch-all element that captures any other information about the location. :param timestamp: UTC timestamp specifying the moment when the reading was taken. :param uri: A URI or URL pointing to information about the location. :param options: Optional form of publish options. """ options = kwargs.get('options', None) ifrom = kwargs.get('ifrom', None) callback = kwargs.get('callback', None) timeout = kwargs.get('timeout', None) for param in ('ifrom', 'block', 'callback', 'timeout', 'options'): if param in kwargs: del kwargs[param] geoloc = Geoloc() geoloc.values = kwargs return self.xmpp['xep_0163'].publish( geoloc, options=options, ifrom=ifrom, callback=callback, timeout=timeout, ) def stop(self, ifrom: Optional[JID] = None, callback: Optional[Callable] = None, timeout: Optional[int] = None) -> Future: """ Clear existing user location information to stop notifications. """ geoloc = Geoloc() return self.xmpp['xep_0163'].publish( geoloc, ifrom=ifrom, callback=callback, timeout=timeout, ) slixmpp/slixmpp/plugins/xep_0080/stanza.py000066400000000000000000000174571477105560000210670ustar00rootroot00000000000000 # 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 slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class Geoloc(ElementBase): """ XMPP's stanza allows entities to know the current geographical or physical location of an entity. (XEP-0080: User Location) Example stanzas: :: 20 Italy 45.44 Venice 12.33 Stanza Interface: :: accuracy -- Horizontal GPS error in meters. alt -- Altitude in meters above or below sea level. area -- A named area such as a campus or neighborhood. bearing -- GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north. building -- A specific building on a street or in an area. country -- The nation where the user is located. countrycode -- The ISO 3166 two-letter country code. datum -- GPS datum. description -- A natural-language name for or description of the location. error -- Horizontal GPS error in arc minutes. Obsoleted by the accuracy parameter. floor -- A particular floor in a building. lat -- Latitude in decimal degrees North. locality -- A locality within the administrative region, such as a town or city. lon -- Longitude in decimal degrees East. postalcode -- A code used for postal delivery. region -- An administrative region of the nation, such as a state or province. room -- A particular room in a building. speed -- The speed at which the entity is moving, in meters per second. street -- A thoroughfare within the locality, or a crossing of two thoroughfares. text -- A catch-all element that captures any other information about the location. timestamp -- UTC timestamp specifying the moment when the reading was taken. uri -- A URI or URL pointing to information about the location. """ namespace = 'http://jabber.org/protocol/geoloc' name = 'geoloc' interfaces = {'accuracy', 'alt', 'area', 'bearing', 'building', 'country', 'countrycode', 'datum', 'dscription', 'error', 'floor', 'lat', 'locality', 'lon', 'postalcode', 'region', 'room', 'speed', 'street', 'text', 'timestamp', 'uri'} sub_interfaces = interfaces plugin_attrib = name def exception(self, e): """ Override exception passback for presence. """ pass def set_accuracy(self, accuracy): """ Set the value of the element. :param accuracy: Horizontal GPS error in meters """ self._set_sub_text('accuracy', text=str(accuracy)) return self def get_accuracy(self): """ Return the value of the element as an integer. """ p = self._get_sub_text('accuracy') if not p: return None else: try: return int(p) except ValueError: return None def set_alt(self, alt): """ Set the value of the element. :param alt: Altitude in meters above or below sea level """ self._set_sub_text('alt', text=str(alt)) return self def get_alt(self): """ Return the value of the element as an integer. """ p = self._get_sub_text('alt') if not p: return None else: try: return int(p) except ValueError: return None def set_bearing(self, bearing): """ Set the value of the element. :param bearing: GPS bearing (direction in which the entity is heading to reach its next waypoint), measured in decimal degrees relative to true north """ self._set_sub_text('bearing', text=str(bearing)) return self def get_bearing(self): """ Return the value of the element as a float. """ p = self._get_sub_text('bearing') if not p: return None else: try: return float(p) except ValueError: return None def set_error(self, error): """ Set the value of the element. :param error: Horizontal GPS error in arc minutes; this element is deprecated in favor of """ self._set_sub_text('error', text=str(error)) return self def get_error(self): """ Return the value of the element as a float. """ p = self._get_sub_text('error') if not p: return None else: try: return float(p) except ValueError: return None def set_lat(self, lat): """ Set the value of the element. :param lat: Latitude in decimal degrees North """ self._set_sub_text('lat', text=str(lat)) return self def get_lat(self): """ Return the value of the element as a float. """ p = self._get_sub_text('lat') if not p: return None else: try: return float(p) except ValueError: return None def set_lon(self, lon): """ Set the value of the element. :param lon: Longitude in decimal degrees East """ self._set_sub_text('lon', text=str(lon)) return self def get_lon(self): """ Return the value of the element as a float. """ p = self._get_sub_text('lon') if not p: return None else: try: return float(p) except ValueError: return None def set_speed(self, speed): """ Set the value of the element. :param speed: The speed at which the entity is moving, in meters per second """ self._set_sub_text('speed', text=str(speed)) return self def get_speed(self): """ Return the value of the element as a float. """ p = self._get_sub_text('speed') if not p: return None else: try: return float(p) except ValueError: return None def set_timestamp(self, timestamp): """ Set the value of the element. :param timestamp: UTC timestamp specifying the moment when the reading was taken """ self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp))) return self def get_timestamp(self): """ Return the value of the element as a DateTime. """ p = self._get_sub_text('timestamp') if not p: return None else: return xep_0082.datetime(p) slixmpp/slixmpp/plugins/xep_0082.py000066400000000000000000000156531477105560000175650ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import datetime as dt from typing import Union from slixmpp.plugins import BasePlugin, register_plugin # ===================================================================== # To make it easier for stanzas without direct access to plugin objects # to use the XEP-0082 utility methods, we will define them as top-level # functions and then just reference them in the plugin itself. def parse(time_str: str) -> dt.datetime: """ Convert a string timestamp into a datetime object. Arguments: time_str -- A formatted timestamp string. """ try: return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f%z') except ValueError: return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z') def format_date(time_obj: Union[dt.datetime, dt.date]) -> str: """ Return a formatted string version of a date object. Format: YYYY-MM-DD Arguments: time_obj -- A date or datetime object. """ if isinstance(time_obj, dt.datetime): time_obj = time_obj.date() return time_obj.isoformat() def format_time(time_obj: Union[dt.datetime, dt.time]) -> str: """ Return a formatted string version of a time object. format: hh:mm:ss[.sss][TZD] arguments: time_obj -- A time or datetime object. """ if isinstance(time_obj, dt.datetime): time_obj = time_obj.timetz() timestamp = time_obj.isoformat() if time_obj.tzinfo == dt.timezone.utc: timestamp = timestamp[:-6] return '%sZ' % timestamp return timestamp def format_datetime(time_obj: dt.datetime) -> str: """ Return a formatted string version of a datetime object. Format: YYYY-MM-DDThh:mm:ss[.sss]TZD arguments: time_obj -- A datetime object. """ timestamp = time_obj.isoformat('T') if time_obj.tzinfo == dt.timezone.utc: timestamp = timestamp[:-6] return '%sZ' % timestamp return timestamp def date(year=None, month=None, day=None, obj=False) -> Union[str, dt.date]: """ Create a date only timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: year -- Integer value of the year (4 digits) month -- Integer value of the month day -- Integer value of the day of the month. obj -- If True, return the date object instead of a formatted string. Defaults to False. """ today = dt.datetime.utcnow() if year is None: year = today.year if month is None: month = today.month if day is None: day = today.day value = dt.date(year, month, day) if obj: return value return format_date(value) def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): """ Create a time only timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: hour -- Integer value of the hour. min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. obj -- If True, return the time object instead of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if hour is None: hour = now.hour if min is None: min = now.minute if sec is None: sec = now.second if micro is None: micro = now.microsecond if offset in (None, 0): offset = dt.timezone.utc elif not isinstance(offset, dt.tzinfo): offset = dt.timezone(dt.timedelta(seconds=offset)) value = dt.time(hour, min, sec, micro, offset) if obj: return value return format_time(value) def datetime(year=None, month=None, day=None, hour=None, min=None, sec=None, micro=None, offset=None, separators=True, obj=False): """ Create a datetime timestamp for the given instant. Unspecified components default to their current counterparts. Arguments: year -- Integer value of the year (4 digits) month -- Integer value of the month day -- Integer value of the day of the month. hour -- Integer value of the hour. min -- Integer value of the number of minutes. sec -- Integer value of the number of seconds. micro -- Integer value of the number of microseconds. offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. obj -- If True, return the datetime object instead of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if year is None: year = now.year if month is None: month = now.month if day is None: day = now.day if hour is None: hour = now.hour if min is None: min = now.minute if sec is None: sec = now.second if micro is None: micro = now.microsecond if offset in (None, 0): offset = dt.timezone.utc elif not isinstance(offset, dt.tzinfo): offset = dt.timezone(dt.timedelta(seconds=offset)) value = dt.datetime(year, month, day, hour, min, sec, micro, offset) if obj: return value return format_datetime(value) class XEP_0082(BasePlugin): """ XEP-0082: XMPP Date and Time Profiles XMPP uses a subset of the formats allowed by ISO 8601 as a matter of pragmatism based on the relatively few formats historically used by the XMPP. Also see . Methods: date -- Create a time stamp using the Date profile. datetime -- Create a time stamp using the DateTime profile. time -- Create a time stamp using the Time profile. format_date -- Format an existing date object. format_datetime -- Format an existing datetime object. format_time -- Format an existing time object. parse -- Convert a time string into a Python datetime object. """ name = 'xep_0082' description = 'XEP-0082: XMPP Date and Time Profiles' dependencies = set() def plugin_init(self): """Start the XEP-0082 plugin.""" self.date = date self.datetime = datetime self.time = time self.format_date = format_date self.format_datetime = format_datetime self.format_time = format_time self.parse = parse register_plugin(XEP_0082) slixmpp/slixmpp/plugins/xep_0084/000077500000000000000000000000001477105560000172035ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0084/__init__.py000066400000000000000000000006261477105560000213200ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0084 import stanza from slixmpp.plugins.xep_0084.stanza import Data, MetaData from slixmpp.plugins.xep_0084.avatar import XEP_0084 register_plugin(XEP_0084) slixmpp/slixmpp/plugins/xep_0084/avatar.py000066400000000000000000000076221477105560000210420ustar00rootroot00000000000000""" Slixmpp: The Slick XMPP Library Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of Slixmpp. See the file LICENSE for copying permission. """ from __future__ import annotations import hashlib import logging from asyncio import Future from typing import ( Dict, Iterable, List, Optional, Set, Union, TYPE_CHECKING, ) from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0084.stanza import Data, MetaData, Pointer from slixmpp.plugins.xep_0084 import stanza try: from typing import TypedDict except ImportError: from typing_extensions import TypedDict class AvatarMetadataItem(TypedDict, total=False): bytes: int id: str type: str height: int width: int url: str MetadataItems = Union[ AvatarMetadataItem, List[AvatarMetadataItem], Set[AvatarMetadataItem] ] log = logging.getLogger(__name__) class XEP_0084(BasePlugin): name = 'xep_0084' description = 'XEP-0084: User Avatar' dependencies = {'xep_0163', 'xep_0060'} stanza = stanza def plugin_init(self): pubsub_stanza = self.xmpp['xep_0060'].stanza register_stanza_plugin(pubsub_stanza.Item, Data) register_stanza_plugin(pubsub_stanza.EventItem, Data) self.xmpp['xep_0060'].map_node_event(Data.namespace, 'avatar_data') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=MetaData.namespace) self.xmpp['xep_0163'].remove_interest(MetaData.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData) def generate_id(self, data) -> str: return hashlib.sha1(data).hexdigest() def retrieve_avatar(self, jid: JID, id: str, **pubsubkwargs) -> Future: """Retrieve an avatar. :param jid: JID of the entity to get the avatar from. :param id: Identifier of the item containing the avatar. """ return self.xmpp['xep_0060'].get_item( jid, Data.namespace, id, **pubsubkwargs ) def publish_avatar(self, data: bytes, **pubsubkwargs) -> Future: """Publish an avatar. :param data: The avatar, in bytes representation. """ payload = Data() payload['value'] = data return self.xmpp['xep_0163'].publish( payload, id=self.generate_id(data), **pubsubkwargs ) def publish_avatar_metadata(self, items: Optional[MetadataItems] = None, pointers: Optional[Iterable[Pointer]] = None, **pubsubkwargs) -> Future: """Publish avatar metadata. :param items: Metadata items to store :param pointers: Optional pointers """ metadata = MetaData() if items is None: items = [] if not isinstance(items, (list, set)): items = [items] for info in items: metadata.add_info(info['id'], info['type'], info['bytes'], height=info.get('height', ''), width=info.get('width', ''), url=info.get('url', '')) if pointers is not None: for pointer in pointers: metadata.add_pointer(pointer) return self.xmpp['xep_0163'].publish( metadata, id=info['id'], **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing avatar metadata information to stop notifications. """ metadata = MetaData() return self.xmpp['xep_0163'].publish( metadata, node=MetaData.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0084/stanza.py000066400000000000000000000053251477105560000210620ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from base64 import b64encode, b64decode from slixmpp.util import bytes from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Data(ElementBase): name = 'data' namespace = 'urn:xmpp:avatar:data' plugin_attrib = 'avatar_data' interfaces = {'value'} def get_value(self): if self.xml.text: return b64decode(bytes(self.xml.text)) return b'' def set_value(self, value): if value: self.xml.text = b64encode(bytes(value)).decode() else: self.xml.text = '' def del_value(self): self.xml.text = '' class MetaData(ElementBase): name = 'metadata' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'avatar_metadata' interfaces = set() def add_info(self, id, itype, ibytes, height=None, width=None, url=None): info = Info() info.values = {'id': id, 'type': itype, 'bytes': '%s' % ibytes, 'height': height, 'width': width, 'url': url} self.append(info) def add_pointer(self, xml): if not isinstance(xml, Pointer): pointer = Pointer() pointer.append(xml) self.append(pointer) else: self.append(xml) class Info(ElementBase): name = 'info' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'info' plugin_multi_attrib = 'items' interfaces = {'bytes', 'height', 'id', 'type', 'url', 'width'} def _get_int(self, name: str) -> int: try: return int(self._get_attr(name)) except ValueError: return 0 def _set_int(self, name: str, value: int): if value not in ('', None): int(value) self._set_attr(name, value) def get_bytes(self) -> int: return self._get_int('bytes') def _set_bytes(self, value: int): self._set_int('bytes', value) def get_height(self) -> int: return self._get_int('height') def set_height(self, value: int): self._set_int('height', value) def get_width(self) -> int: return self._get_int('width') def set_width(self, value: int): self._set_int('width', value) class Pointer(ElementBase): name = 'pointer' namespace = 'urn:xmpp:avatar:metadata' plugin_attrib = 'pointer' plugin_multi_attrib = 'pointers' interfaces = set() register_stanza_plugin(MetaData, Info, iterable=True) register_stanza_plugin(MetaData, Pointer, iterable=True) slixmpp/slixmpp/plugins/xep_0085/000077500000000000000000000000001477105560000172045ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0085/__init__.py000066400000000000000000000005501477105560000213150ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0085.stanza import ChatState from slixmpp.plugins.xep_0085.chat_states import XEP_0085 register_plugin(XEP_0085) slixmpp/slixmpp/plugins/xep_0085/chat_states.py000066400000000000000000000031461477105560000220640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio import logging import slixmpp from slixmpp.stanza import Message from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0085 import stanza, ChatState log = logging.getLogger(__name__) class XEP_0085(BasePlugin): """ XEP-0085 Chat State Notifications """ name = 'xep_0085' description = 'XEP-0085: Chat State Notifications' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): self.xmpp.register_handler( Callback('Chat State', StanzaPath('message/chat_state'), self._handle_chat_state)) register_stanza_plugin(Message, stanza.Active) register_stanza_plugin(Message, stanza.Composing) register_stanza_plugin(Message, stanza.Gone) register_stanza_plugin(Message, stanza.Inactive) register_stanza_plugin(Message, stanza.Paused) def plugin_end(self): self.xmpp.remove_handler('Chat State') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace) def _handle_chat_state(self, msg): state = msg['chat_state'] log.debug("Chat State: %s, %s", state, msg['from'].jid) self.xmpp.event('chatstate', msg) self.xmpp.event('chatstate_%s' % state, msg) slixmpp/slixmpp/plugins/xep_0085/stanza.py000066400000000000000000000037641477105560000210700ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permissio import slixmpp from slixmpp.xmlstream import ElementBase, ET class ChatState(ElementBase): """ Example chat state stanzas: :: """ name = '' namespace = 'http://jabber.org/protocol/chatstates' plugin_attrib = 'chat_state' interfaces = {'chat_state'} sub_interfaces = interfaces is_extension = True states = {'active', 'composing', 'gone', 'inactive', 'paused'} def setup(self, xml=None): self.xml = ET.Element('') return True def get_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.xml.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = state_xml return state return '' def set_chat_state(self, state): self.del_chat_state() parent = self.parent() if state in self.states: self.xml = ET.Element('{%s}%s' % (self.namespace, state)) parent.append(self.xml) elif state not in [None, '']: raise ValueError('Invalid chat state') def del_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.xml.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = ET.Element('') parent.xml.remove(state_xml) class Active(ChatState): name = 'active' class Composing(ChatState): name = 'composing' class Gone(ChatState): name = 'gone' class Inactive(ChatState): name = 'inactive' class Paused(ChatState): name = 'paused' slixmpp/slixmpp/plugins/xep_0086/000077500000000000000000000000001477105560000172055ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0086/__init__.py000066400000000000000000000005551477105560000213230ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0086.stanza import LegacyError from slixmpp.plugins.xep_0086.legacy_error import XEP_0086 register_plugin(XEP_0086) slixmpp/slixmpp/plugins/xep_0086/legacy_error.py000066400000000000000000000026371477105560000222440ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Error from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0086 import stanza, LegacyError class XEP_0086(BasePlugin): """ XEP-0086: Error Condition Mappings Older XMPP implementations used code based error messages, similar to HTTP response codes. Since then, error condition elements have been introduced. XEP-0086 provides a mapping between the new condition elements and a combination of error types and the older response codes. Also see . Configuration Values: :: override -- Indicates if applying legacy error codes should be done automatically. Defaults to True. If False, then inserting legacy error codes can be done using: iq['error']['legacy']['condition'] = ... """ name = 'xep_0086' description = 'XEP-0086: Error Condition Mappings' dependencies = set() stanza = stanza default_config = { 'override': True } def plugin_init(self): register_stanza_plugin(Error, LegacyError, overrides=self.override) slixmpp/slixmpp/plugins/xep_0086/stanza.py000066400000000000000000000060051477105560000210600ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin class LegacyError(ElementBase): """ Older XMPP implementations used code based error messages, similar to HTTP response codes. Since then, error condition elements have been introduced. XEP-0086 provides a mapping between the new condition elements and a combination of error types and the older response codes. Also see . Example legacy error stanzas: :: :var error_map: A map of error conditions to error types and code values. """ name = 'legacy' namespace = Error.namespace plugin_attrib = name interfaces = {'condition'} overrides = ['set_condition'] error_map = {'bad-request': ('modify', '400'), 'conflict': ('cancel', '409'), 'feature-not-implemented': ('cancel', '501'), 'forbidden': ('auth', '403'), 'gone': ('modify', '302'), 'internal-server-error': ('wait', '500'), 'item-not-found': ('cancel', '404'), 'jid-malformed': ('modify', '400'), 'not-acceptable': ('modify', '406'), 'not-allowed': ('cancel', '405'), 'not-authorized': ('auth', '401'), 'payment-required': ('auth', '402'), 'recipient-unavailable': ('wait', '404'), 'redirect': ('modify', '302'), 'registration-required': ('auth', '407'), 'remote-server-not-found': ('cancel', '404'), 'remote-server-timeout': ('wait', '504'), 'resource-constraint': ('wait', '500'), 'service-unavailable': ('cancel', '503'), 'subscription-required': ('auth', '407'), 'undefined-condition': (None, '500'), 'unexpected-request': ('wait', '400')} def setup(self, xml): """Don't create XML for the plugin.""" self.xml = ET.Element('') def set_condition(self, value): """ Set the error type and code based on the given error condition value. :param value: The new error condition. """ self.parent().set_condition(value) error_data = self.error_map.get(value, None) if error_data is not None: if error_data[0] is not None: self.parent()['type'] = error_data[0] self.parent()['code'] = error_data[1] slixmpp/slixmpp/plugins/xep_0091/000077500000000000000000000000001477105560000172015ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0091/__init__.py000066400000000000000000000006311477105560000213120ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0091 import stanza from slixmpp.plugins.xep_0091.stanza import LegacyDelay from slixmpp.plugins.xep_0091.legacy_delay import XEP_0091 register_plugin(XEP_0091) slixmpp/slixmpp/plugins/xep_0091/legacy_delay.py000066400000000000000000000013061477105560000221750ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0091 import stanza class XEP_0091(BasePlugin): """ XEP-0091: Legacy Delayed Delivery """ name = 'xep_0091' description = 'XEP-0091: Legacy Delayed Delivery' dependencies = set() stanza = stanza def plugin_init(self): register_stanza_plugin(Message, stanza.LegacyDelay) register_stanza_plugin(Presence, stanza.LegacyDelay) slixmpp/slixmpp/plugins/xep_0091/stanza.py000066400000000000000000000022611477105560000210540ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import datetime as dt from slixmpp.jid import JID from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class LegacyDelay(ElementBase): name = 'x' namespace = 'jabber:x:delay' plugin_attrib = 'legacy_delay' interfaces = {'from', 'stamp', 'text'} def get_from(self): from_ = self._get_attr('from') return JID(from_) if from_ else None def set_from(self, value): self._set_attr('from', str(value)) def get_stamp(self): timestamp = self._get_attr('stamp') return xep_0082.parse('%sZ' % timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): value = value.astimezone(dt.timezone.utc) value = xep_0082.format_datetime(value) self._set_attr('stamp', value[0:19].replace('-', '')) def get_text(self): return self.xml.text def set_text(self, value): self.xml.text = value def del_text(self): self.xml.text = '' slixmpp/slixmpp/plugins/xep_0092/000077500000000000000000000000001477105560000172025ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0092/__init__.py000066400000000000000000000006201477105560000213110ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0092 import stanza from slixmpp.plugins.xep_0092.stanza import Version from slixmpp.plugins.xep_0092.version import XEP_0092 register_plugin(XEP_0092) slixmpp/slixmpp/plugins/xep_0092/stanza.py000066400000000000000000000022001477105560000210460ustar00rootroot00000000000000 # 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 slixmpp.xmlstream import ElementBase, ET class Version(ElementBase): """ XMPP allows for an agent to advertise the name and version of the underlying software libraries, as well as the operating system that the agent is running on. Example version stanzas: :: Slixmpp 1.0 Linux Stanza Interface: :: name -- The human readable name of the software. version -- The specific version of the software. os -- The name of the operating system running the program. """ name = 'query' namespace = 'jabber:iq:version' plugin_attrib = 'software_version' interfaces = {'name', 'version', 'os'} sub_interfaces = interfaces slixmpp/slixmpp/plugins/xep_0092/version.py000066400000000000000000000046761477105560000212560ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional import slixmpp from slixmpp import JID from slixmpp.stanza import Iq from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0092 import Version, stanza log = logging.getLogger(__name__) class XEP_0092(BasePlugin): """ XEP-0092: Software Version """ name = 'xep_0092' description = 'XEP-0092: Software Version' dependencies = {'xep_0030'} stanza = stanza default_config = { 'software_name': 'Slixmpp', 'version': slixmpp.__version__, 'os': '' } def plugin_init(self): """ Start the XEP-0092 plugin. """ if 'name' in self.config: self.software_name = self.config['name'] self.xmpp.register_handler( Callback('Software Version', StanzaPath('iq@type=get/software_version'), self._handle_version)) register_stanza_plugin(Iq, Version) def plugin_end(self): self.xmpp.remove_handler('Software Version') self.xmpp['xep_0030'].del_feature(feature='jabber:iq:version') def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') def _handle_version(self, iq: Iq): """ Respond to a software version query. :param iq: The Iq stanza containing the software version query. """ iq = iq.reply() if self.software_name: iq['software_version']['name'] = self.software_name iq['software_version']['version'] = self.version iq['software_version']['os'] = self.os else: iq.error() iq['error']['type'] = 'cancel' iq['error']['condition'] = 'service-unavailable' iq.send() def get_version(self, jid: JID, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """ Retrieve the software version of a remote agent. :param jid: The JID of the entity to query. """ iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) iq['query'] = Version.namespace return iq.send(**iqkwargs) slixmpp/slixmpp/plugins/xep_0095/000077500000000000000000000000001477105560000172055ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0095/__init__.py000066400000000000000000000006251477105560000213210ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0095 import stanza from slixmpp.plugins.xep_0095.stanza import SI from slixmpp.plugins.xep_0095.stream_initiation import XEP_0095 register_plugin(XEP_0095) slixmpp/slixmpp/plugins/xep_0095/stanza.py000066400000000000000000000011571477105560000210630ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class SI(ElementBase): name = 'si' namespace = 'http://jabber.org/protocol/si' plugin_attrib = 'si' interfaces = {'id', 'mime_type', 'profile'} def get_mime_type(self): return self._get_attr('mime-type', 'application/octet-stream') def set_mime_type(self, value): self._set_attr('mime-type', value) def del_mime_type(self): self._del_attr('mime-type') slixmpp/slixmpp/plugins/xep_0095/stream_initiation.py000066400000000000000000000162041477105560000233040ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging import threading from uuid import uuid4 from slixmpp import Iq, Message from slixmpp.exceptions import XMPPError from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0095 import stanza, SI log = logging.getLogger(__name__) SOCKS5 = 'http://jabber.org/protocol/bytestreams' IBB = 'http://jabber.org/protocol/ibb' class XEP_0095(BasePlugin): name = 'xep_0095' description = 'XEP-0095: Stream Initiation' dependencies = {'xep_0020', 'xep_0030', 'xep_0047', 'xep_0065'} stanza = stanza def plugin_init(self): self._profiles = {} self._methods = {} self._methods_order = [] self._pending_lock = threading.Lock() self._pending= {} self.register_method(SOCKS5, 'xep_0065', 100) self.register_method(IBB, 'xep_0047', 50) register_stanza_plugin(Iq, SI) register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation) self.xmpp.register_handler( Callback('SI Request', StanzaPath('iq@type=set/si'), self._handle_request)) self.api.register(self._add_pending, 'add_pending', default=True) self.api.register(self._get_pending, 'get_pending', default=True) self.api.register(self._del_pending, 'del_pending', default=True) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(SI.namespace) def plugin_end(self): self.xmpp.remove_handler('SI Request') self.xmpp['xep_0030'].del_feature(feature=SI.namespace) def register_profile(self, profile_name, plugin): self._profiles[profile_name] = plugin def unregister_profile(self, profile_name): try: del self._profiles[profile_name] except KeyError: pass def register_method(self, method, plugin_name, order=50): self._methods[method] = (plugin_name, order) self._methods_order.append((order, method, plugin_name)) self._methods_order.sort() def unregister_method(self, method): if method in self._methods: plugin_name, order = self._methods[method] del self._methods[method] self._methods_order.remove((order, method, plugin_name)) self._methods_order.sort() async def _handle_request(self, iq): profile = iq['si']['profile'] sid = iq['si']['id'] if not sid: raise XMPPError(etype='modify', condition='bad-request') if profile not in self._profiles: raise XMPPError( etype='modify', condition='bad-request', extension='bad-profile', extension_ns=SI.namespace) neg = iq['si']['feature_neg']['form'].get_fields() options = neg['stream-method']['options'] or [] methods = [] for opt in options: methods.append(opt['value']) for method in methods: if method in self._methods: supported = True break else: raise XMPPError('bad-request', extension='no-valid-streams', extension_ns=SI.namespace) selected_method = None log.debug('Available: %s', methods) for order, method, plugin in self._methods_order: log.debug('Testing: %s', method) if method in methods: selected_method = method break receiver = iq['to'] sender = iq['from'] await self.api['add_pending'](receiver, sid, sender, { 'response_id': iq['id'], 'method': selected_method, 'profile': profile }) self.xmpp.event('si_request', iq) def offer(self, jid, sid=None, mime_type=None, profile=None, methods=None, payload=None, ifrom=None, **iqargs): if sid is None: sid = uuid4().hex if methods is None: methods = list(self._methods.keys()) if not isinstance(methods, (list, tuple, set)): methods = [methods] si = self.xmpp.Iq() si['to'] = jid si['from'] = ifrom si['type'] = 'set' si['si']['id'] = sid si['si']['mime_type'] = mime_type si['si']['profile'] = profile if not isinstance(payload, (list, tuple, set)): payload = [payload] for item in payload: si['si'].append(item) si['si']['feature_neg']['form'].add_field( var='stream-method', ftype='list-single', options=methods) return si.send(**iqargs) async def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None): """Accept a stream initiation. .. versionchanged:: 1.8.0 This function is now a coroutine. """ stream = await self.api['get_pending'](ifrom, sid, jid) iq = self.xmpp.Iq() iq['id'] = stream['response_id'] iq['to'] = jid iq['from'] = ifrom iq['type'] = 'result' if payload: iq['si'].append(payload) iq['si']['feature_neg']['form']['type'] = 'submit' iq['si']['feature_neg']['form'].add_field( var='stream-method', ftype='list-single', value=stream['method']) if ifrom is None: ifrom = self.xmpp.boundjid method_plugin = self._methods[stream['method']][0] self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid) await self.api['del_pending'](ifrom, sid, jid) if stream_handler: self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid), stream_handler, disposable=True) return await iq.send() async def decline(self, jid, sid, ifrom=None): """Decline a stream initiation. .. versionchanged:: 1.8.0 This function is now a coroutine. """ stream = await self.api['get_pending'](ifrom, sid, jid) if not stream: return iq = self.xmpp.Iq() iq['id'] = stream['response_id'] iq['to'] = jid iq['from'] = ifrom iq['type'] = 'error' iq['error']['condition'] = 'forbidden' iq['error']['text'] = 'Offer declined' await self.api['del_pending'](ifrom, sid, jid) return await iq.send() def _add_pending(self, jid, node, ifrom, data): with self._pending_lock: self._pending[(jid, node, ifrom)] = data def _get_pending(self, jid, node, ifrom, data): with self._pending_lock: return self._pending.get((jid, node, ifrom), None) def _del_pending(self, jid, node, ifrom, data): with self._pending_lock: if (jid, node, ifrom) in self._pending: del self._pending[(jid, node, ifrom)] slixmpp/slixmpp/plugins/xep_0096/000077500000000000000000000000001477105560000172065ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0096/__init__.py000066400000000000000000000006231477105560000213200ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0096 import stanza from slixmpp.plugins.xep_0096.stanza import File from slixmpp.plugins.xep_0096.file_transfer import XEP_0096 register_plugin(XEP_0096) slixmpp/slixmpp/plugins/xep_0096/file_transfer.py000066400000000000000000000033441477105560000224070ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Iq, Message from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0096 import stanza, File log = logging.getLogger(__name__) class XEP_0096(BasePlugin): name = 'xep_0096' description = 'XEP-0096: SI File Transfer' dependencies = {'xep_0095'} stanza = stanza def plugin_init(self): register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File) self.xmpp['xep_0095'].register_profile(File.namespace, self) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(File.namespace) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=File.namespace) self.xmpp['xep_0095'].unregister_profile(File.namespace, self) def request_file_transfer(self, jid, sid=None, name=None, size=None, desc=None, hash=None, date=None, allow_ranged=False, mime_type=None, **iqargs): data = File() data['name'] = name data['size'] = size data['date'] = date data['desc'] = desc data['hash'] = hash if allow_ranged: data.enable('range') return self.xmpp['xep_0095'].offer(jid, sid=sid, mime_type=mime_type, profile=File.namespace, payload=data, **iqargs) slixmpp/slixmpp/plugins/xep_0096/stanza.py000066400000000000000000000023501477105560000210600ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import datetime as dt from slixmpp.xmlstream import ElementBase, register_stanza_plugin from slixmpp.plugins import xep_0082 class File(ElementBase): name = 'file' namespace = 'http://jabber.org/protocol/si/profile/file-transfer' plugin_attrib = 'file' interfaces = {'name', 'size', 'date', 'hash', 'desc'} sub_interfaces = {'desc'} def set_size(self, value): self._set_attr('size', str(value)) def get_date(self): timestamp = self._get_attr('date') return xep_0082.parse(timestamp) def set_date(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) self._set_attr('date', value) class Range(ElementBase): name = 'range' namespace = 'http://jabber.org/protocol/si/profile/file-transfer' plugin_attrib = 'range' interfaces = {'length', 'offset'} def set_length(self, value): self._set_attr('length', str(value)) def set_offset(self, value): self._set_attr('offset', str(value)) register_stanza_plugin(File, Range) slixmpp/slixmpp/plugins/xep_0100/000077500000000000000000000000001477105560000171705ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0100/__init__.py000066400000000000000000000002211477105560000212740ustar00rootroot00000000000000from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0100.gateway import XEP_0100, LegacyError register_plugin(XEP_0100) slixmpp/slixmpp/plugins/xep_0100/gateway.py000066400000000000000000000216611477105560000212110ustar00rootroot00000000000000import asyncio import logging from functools import partial import typing from slixmpp import Message, Iq, Presence, JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin class XEP_0100(BasePlugin): """ XEP-0100: Gateway interaction Does not cover the deprecated Agent Information and 'jabber:iq:gateway' protocols Events registered by this plugin: - legacy_login: Jabber user got online or just registered - legacy_logout: Jabber user got offline or just unregistered - legacy_presence_unavailable: Jabber user sent an unavailable presence to a legacy contact - gateway_message: Jabber user sent a direct message to the gateway component - legacy_message: Jabber user sent a message to the legacy network Plugin Parameters: - `component_name`: (str) Name of the entity - `type`: (str) Type of the gateway identity. Should be the name of the legacy service - `needs_registration`: (bool) If set to True, messages received from unregistered users will not be transmitted to the legacy service API: - legacy_contact_add(jid, node, ifrom: JID, args: JID): Add contact on the legacy service. Should raise LegacyError if anything goes wrong in the process. `ifrom` is the gateway user's JID and `args` is the legacy contact's JID. - legacy_contact_remove(jid, node, ifrom: JID, args: JID): Remove a contact. """ name = "xep_0100" description = "XEP-0100: Gateway interaction" dependencies = { "xep_0030", # Service discovery "xep_0077", # In band registration } default_config = { "component_name": "SliXMPP gateway", "type": "xmpp", "needs_registration": True, } def plugin_init(self): if not self.xmpp.is_component: log.error("Only components can be gateways, aborting plugin load") return self.xmpp["xep_0030"].add_identity( name=self.component_name, category="gateway", itype=self.type ) self.api.register(self._legacy_contact_remove, "legacy_contact_remove") self.api.register(self._legacy_contact_add, "legacy_contact_add") # Without that BaseXMPP sends unsub/unavailable on sub requests and we don't want that self.xmpp.client_roster.auto_authorize = True self.xmpp.client_roster.auto_subscribe = False self.xmpp.add_event_handler("user_register", self.on_user_register) self.xmpp.add_event_handler("user_unregister", self.on_user_unregister) self.xmpp.add_event_handler("presence_available", self.on_presence_available) self.xmpp.add_event_handler( "presence_unavailable", self.on_presence_unavailable ) self.xmpp.add_event_handler("presence_subscribe", self.on_presence_subscribe) self.xmpp.add_event_handler( "presence_unsubscribe", self.on_presence_unsubscribe ) self.xmpp.add_event_handler("message", self.on_message) def plugin_end(self): if not self.xmpp.is_component: return self.xmpp.del_event_handler("user_register", self.on_user_register) self.xmpp.del_event_handler("user_unregister", self.on_user_unregister) self.xmpp.del_event_handler("presence_available", self.on_presence_available) self.xmpp.del_event_handler( "presence_unavailable", self.on_presence_unavailable ) self.xmpp.del_event_handler("presence_subscribe", self.on_presence_subscribe) self.xmpp.del_event_handler("message", self.on_message) self.xmpp.del_event_handler( "presence_unsubscribe", self.on_presence_unsubscribe ) async def get_user(self, stanza): return await self.xmpp["xep_0077"].api["user_get"](None, None, None, stanza) def send_presence(self, pto, ptype=None, pstatus=None, pfrom=None): self.xmpp.send_presence( pfrom=self.xmpp.boundjid.bare, ptype=ptype, pto=pto, pstatus=pstatus, ) async def on_user_register(self, iq: Iq): user_jid = iq["from"] user = await self.get_user(iq) if user is None: # This should not happen log.warning(f"{user_jid} has registered but cannot find them in user store") else: log.debug(f"Sending subscription request to {user_jid}") self.xmpp.client_roster.subscribe(user_jid) def on_user_unregister(self, iq: Iq): user_jid = iq["from"] log.debug(f"Sending subscription request to {user_jid}") self.xmpp.event("legacy_logout", iq) self.xmpp.client_roster.unsubscribe(iq["from"]) self.xmpp.client_roster.remove(iq["from"]) log.debug(f"roster: {self.xmpp.client_roster}") async def on_presence_available(self, presence: Presence): user_jid = presence["from"] user = await self.get_user(presence) if user is None: log.warning( f"{user_jid} has gotten online but cannot find them in user store" ) else: self.xmpp.event("legacy_login", presence) log.debug(f"roster: {self.xmpp.client_roster}") self.send_presence(pto=user_jid.bare, ptype="available") async def on_presence_unavailable(self, presence: Presence): user_jid = presence["from"] user = await self.get_user(presence) if user is None: # This should not happen log.warning( f"{user_jid} has gotten offline but but cannot find them in user store" ) return if presence["to"] == self.xmpp.boundjid.bare: self.xmpp.event("legacy_logout", presence) self.send_presence(pto=user_jid, ptype="unavailable") else: self.xmpp.event("legacy_presence_unavailable", presence) async def _legacy_contact_add(self, jid, node, ifrom, contact_jid: JID): pass async def on_presence_subscribe(self, presence: Presence): user_jid = presence["from"] user = await self.get_user(presence) if user is None and self.needs_registration: return if presence["to"] == self.xmpp.boundjid.bare: return try: await self.api["legacy_contact_add"]( ifrom=user_jid, args=presence["to"], ) except LegacyError: self.xmpp.send_presence( pfrom=presence["to"], ptype="unsubscribed", pto=user_jid, ) return self.xmpp.send_presence( pfrom=presence["to"], ptype="subscribed", pto=user_jid, ) self.xmpp.send_presence( pfrom=presence["to"], pto=user_jid, ) self.xmpp.send_presence( pfrom=presence["to"], ptype="subscribe", pto=user_jid, ) # TODO: handle resulting subscribed presences async def on_presence_unsubscribe(self, presence: Presence): if presence["to"] == self.xmpp.boundjid.bare: # should we trigger unregistering here? return user_jid = presence["from"] user = await self.get_user(presence) if user is None: log.debug("Received remove subscription from unregistered user") if self.needs_registration: return await self.api["legacy_contact_remove"](ifrom=user_jid, args=presence["to"]) for ptype in "unsubscribe", "unsubscribed", "unavailable": self.xmpp.send_presence( pfrom=presence["to"], ptype=ptype, pto=user_jid, ) async def _legacy_contact_remove(self, jid, node, ifrom, contact_jid: JID): pass async def on_message(self, msg: Message): if msg["type"] == "groupchat": return # groupchat messages are out of scope of XEP-0100 if msg["to"] == self.xmpp.boundjid.bare: # It may be useful to exchange direct messages with the component self.xmpp.event("gateway_message", msg) return if self.needs_registration and await self.get_user(msg) is None: return self.xmpp.event("legacy_message", msg) def transform_legacy_message( self, jabber_user_jid: typing.Union[JID, str], legacy_contact_id: str, body: str, mtype: typing.Optional[str] = None, ): """ Transform a legacy message to an XMPP message """ # Should escaping legacy IDs to valid JID local parts be handled here? # Maybe by internal API stuff? self.xmpp.send_message( mfrom=JID(f"{legacy_contact_id}@{self.xmpp.boundjid.bare}"), mto=JID(jabber_user_jid).bare, mbody=body, mtype=mtype, ) class LegacyError(Exception): pass log = logging.getLogger(__name__) slixmpp/slixmpp/plugins/xep_0106.py000066400000000000000000000011101477105560000175410ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins import BasePlugin, register_plugin class XEP_0106(BasePlugin): name = 'xep_0106' description = 'XEP-0106: JID Escaping' dependencies = {'xep_0030'} def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping') def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping') register_plugin(XEP_0106) slixmpp/slixmpp/plugins/xep_0107/000077500000000000000000000000001477105560000171775ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0107/__init__.py000066400000000000000000000006231477105560000213110ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0107 import stanza from slixmpp.plugins.xep_0107.stanza import UserMood from slixmpp.plugins.xep_0107.user_mood import XEP_0107 register_plugin(XEP_0107) slixmpp/slixmpp/plugins/xep_0107/stanza.py000066400000000000000000000042251477105560000210540ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class UserMood(ElementBase): name = 'mood' namespace = 'http://jabber.org/protocol/mood' plugin_attrib = 'mood' interfaces = {'value', 'text'} sub_interfaces = {'text'} moods = {'afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious', 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious', 'cold', 'confident', 'confused', 'contemplative', 'contented', 'cranky', 'crazy', 'creative', 'curious', 'dejected', 'depressed', 'disappointed', 'disgusted', 'dismayed', 'distracted', 'embarrassed', 'envious', 'excited', 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy', 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated', 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love', 'indignant', 'interested', 'intoxicated', 'invincible', 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody', 'nervous', 'neutral', 'offended', 'outraged', 'playful', 'proud', 'relaxed', 'relieved', 'remorseful', 'restless', 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked', 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong', 'surprised', 'thankful', 'thirsty', 'tired', 'undefined', 'weak', 'worried'} def set_value(self, value): self.del_value() if value in self.moods: self._set_sub_text(value, '', keep=True) else: raise ValueError('Unknown mood value') def get_value(self): for child in self.xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.moods: return elem_name return '' def del_value(self): curr_value = self.get_value() if curr_value: self._set_sub_text(curr_value, '', keep=False) slixmpp/slixmpp/plugins/xep_0107/user_mood.py000066400000000000000000000036401477105560000215500ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import ( Optional, ) from slixmpp import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0107 import stanza, UserMood log = logging.getLogger(__name__) class XEP_0107(BasePlugin): """ XEP-0107: User Mood """ name = 'xep_0107' description = 'XEP-0107: User Mood' dependencies = {'xep_0163'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, UserMood) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserMood.namespace) self.xmpp['xep_0163'].remove_interest(UserMood.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_mood', UserMood) def publish_mood(self, value: Optional[str] = None, text: Optional[str] = None, **pubsubkwargs) -> Future: """ Publish the user's current mood. :param value: The name of the mood to publish. :param text: Optional natural-language description or reason for the mood. """ mood = UserMood() mood['value'] = value mood['text'] = text return self.xmpp['xep_0163'].publish( mood, node=UserMood.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user mood information to stop notifications. """ mood = UserMood() return self.xmpp['xep_0163'].publish( mood, node=UserMood.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0108/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0108/__init__.py000066400000000000000000000006331477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0108 import stanza from slixmpp.plugins.xep_0108.stanza import UserActivity from slixmpp.plugins.xep_0108.user_activity import XEP_0108 register_plugin(XEP_0108) slixmpp/slixmpp/plugins/xep_0108/stanza.py000066400000000000000000000066021477105560000210560ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class UserActivity(ElementBase): name = 'activity' namespace = 'http://jabber.org/protocol/activity' plugin_attrib = 'activity' interfaces = {'value', 'text'} sub_interfaces = {'text'} general = {'doing_chores', 'drinking', 'eating', 'exercising', 'grooming', 'having_appointment', 'inactive', 'relaxing', 'talking', 'traveling', 'undefined', 'working'} specific = {'at_the_spa', 'brushing_teeth', 'buying_groceries', 'cleaning', 'coding', 'commuting', 'cooking', 'cycling', 'dancing', 'day_off', 'doing_maintenance', 'doing_the_dishes', 'doing_the_laundry', 'driving', 'fishing', 'gaming', 'gardening', 'getting_a_haircut', 'going_out', 'hanging_out', 'having_a_beer', 'having_a_snack', 'having_breakfast', 'having_coffee', 'having_dinner', 'having_lunch', 'having_tea', 'hiding', 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life', 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train', 'on_a_trip', 'on_the_phone', 'on_vacation', 'on_video_phone', 'other', 'partying', 'playing_sports', 'praying', 'reading', 'rehearsing', 'running', 'running_an_errand', 'scheduled_holiday', 'shaving', 'shopping', 'skiing', 'sleeping', 'smoking', 'socializing', 'studying', 'sunbathing', 'swimming', 'taking_a_bath', 'taking_a_shower', 'thinking', 'walking', 'walking_the_dog', 'watching_a_movie', 'watching_tv', 'working_out', 'writing'} def set_value(self, value): self.del_value() general = value specific = None if isinstance(value, tuple) or isinstance(value, list): general = value[0] specific = value[1] if general in self.general: gen_xml = ET.Element('{%s}%s' % (self.namespace, general)) if specific: spec_xml = ET.Element('{%s}%s' % (self.namespace, specific)) if specific in self.specific: gen_xml.append(spec_xml) else: raise ValueError('Unknown specific activity') self.xml.append(gen_xml) else: raise ValueError('Unknown general activity') def get_value(self): general = None specific = None gen_xml = None for child in self.xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.general: general = elem_name gen_xml = child if gen_xml is not None: for child in gen_xml: if child.tag.startswith('{%s}' % self.namespace): elem_name = child.tag.split('}')[-1] if elem_name in self.specific: specific = elem_name return (general, specific) def del_value(self): curr_value = self.get_value() if curr_value[0]: self._set_sub_text(curr_value[0], '', keep=False) slixmpp/slixmpp/plugins/xep_0108/user_activity.py000066400000000000000000000036551477105560000224550ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0108 import stanza, UserActivity log = logging.getLogger(__name__) class XEP_0108(BasePlugin): """ XEP-0108: User Activity """ name = 'xep_0108' description = 'XEP-0108: User Activity' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserActivity.namespace) self.xmpp['xep_0163'].remove_interest(UserActivity.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_activity', UserActivity) def publish_activity(self, general: str, specific: Optional[str] = None, text: Optional[str] = None, **pubsubkwargs) -> Future: """ Publish the user's current activity. :param general: The required general category of the activity. :param specific: Optional specific activity being done as part of the general category. :param text: Optional natural-language description or reason for the activity. """ activity = UserActivity() activity['value'] = (general, specific) activity['text'] = text return self.xmpp['xep_0163'].publish( activity, node=UserActivity.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user activity information to stop notifications. """ activity = UserActivity() return self.xmpp['xep_0163'].publish( activity, node=UserActivity.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0115/000077500000000000000000000000001477105560000171765ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0115/__init__.py000066400000000000000000000006351477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0115.stanza import Capabilities from slixmpp.plugins.xep_0115.static import StaticCaps from slixmpp.plugins.xep_0115.caps import XEP_0115 register_plugin(XEP_0115) slixmpp/slixmpp/plugins/xep_0115/caps.py000066400000000000000000000327601477105560000205060ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging import hashlib import base64 from asyncio import Future, Lock from collections import defaultdict from typing import Optional from slixmpp import __version__ from slixmpp.stanza import StreamFeatures, Presence, Iq from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.util import MemoryCache from slixmpp.exceptions import XMPPError from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0115 import stanza, StaticCaps from slixmpp.types import OptJidStr log = logging.getLogger(__name__) class XEP_0115(BasePlugin): """ XEP-0115: Entity Capabilities """ name = 'xep_0115' description = 'XEP-0115: Entity Capabilities' dependencies = {'xep_0030', 'xep_0128', 'xep_0004'} stanza = stanza default_config = { 'hash': 'sha-1', 'caps_node': None, 'broadcast': True, 'cache': None, } def plugin_init(self): self.hashes = {'sha-1': hashlib.sha1, 'sha1': hashlib.sha1, 'md5': hashlib.md5} if self.caps_node is None: self.caps_node = 'http://slixmpp.com/ver/%s' % __version__ if self.cache is None: self.cache = MemoryCache() register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities) self._disco_ops = ['cache_caps', 'get_caps', 'assign_verstring', 'get_verstring', 'supports', 'has_identity'] self.xmpp.register_handler( Callback('Entity Capabilites', StanzaPath('presence/caps'), self._handle_caps)) self.xmpp.add_filter('out', self._filter_add_caps) self.xmpp.add_event_handler('entity_caps', self._process_caps) if not self.xmpp.is_component: self.xmpp.register_feature('caps', self._handle_caps_feature, restart=False, order=10010) disco = self.xmpp['xep_0030'] self.static = StaticCaps(self.xmpp, disco.static) for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) for op in ('supports', 'has_identity'): self.xmpp['xep_0030'].api.register(getattr(self.static, op), op) self._run_node_handler = disco._run_node_handler disco.cache_caps = self.cache_caps disco.update_caps = self.update_caps disco.assign_verstring = self.assign_verstring disco.get_verstring = self.get_verstring # prevent concurrent fetches for the same hash self._locks = defaultdict(Lock) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) self.xmpp.del_filter('out', self._filter_add_caps) self.xmpp.del_event_handler('entity_caps', self._process_caps) self.xmpp.remove_handler('Entity Capabilities') if not self.xmpp.is_component: self.xmpp.unregister_feature('caps', 10010) for op in ('supports', 'has_identity'): self.xmpp['xep_0030'].restore_defaults(op) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) async def _filter_add_caps(self, stanza): if not isinstance(stanza, Presence) or not self.broadcast: return stanza if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'): return stanza ver = await self.get_verstring(stanza['from']) if ver: stanza['caps']['node'] = self.caps_node stanza['caps']['hash'] = self.hash stanza['caps']['ver'] = ver return stanza def _handle_caps(self, presence): if not self.xmpp.is_component: if presence['from'] == self.xmpp.boundjid: return self.xmpp.event('entity_caps', presence) def _handle_caps_feature(self, features): # We already have a method to process presence with # caps, so wrap things up and use that. p = Presence() p['from'] = self.xmpp.boundjid.domain p.append(features['caps']) self.xmpp.features.add('caps') self.xmpp.event('entity_caps', p) async def _process_caps(self, pres: Presence): if not pres['caps']['hash']: log.debug("Received unsupported legacy caps: %s, %s, %s", pres['caps']['node'], pres['caps']['ver'], pres['caps']['ext']) self.xmpp.event('entity_caps_legacy', pres) return ver = pres['caps']['ver'] async with self._locks[ver]: await self._process_caps_wrapped(pres, ver) self._locks.pop(ver, None) async def _process_caps_wrapped(self, pres: Presence, ver: str): existing_verstring = await self.get_verstring(pres['from'].full) if str(existing_verstring) == str(ver): return existing_caps = await self.get_caps(verstring=ver) if existing_caps is not None: await 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']) await self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom) return except XMPPError: return log.debug("New caps verification string: %s", ver) try: node = '%s#%s' % (pres['caps']['node'], ver) caps = await self.xmpp['xep_0030'].get_info(pres['from'], node, ifrom=ifrom) if isinstance(caps, Iq): caps = caps['disco_info'] if await self._validate_caps(caps, pres['caps']['hash'], pres['caps']['ver']): await self.assign_verstring(pres['from'], pres['caps']['ver']) except XMPPError: log.debug("Could not retrieve disco#info results for caps for %s", node) async def _validate_caps(self, caps, hash, check_verstring): # Check Identities full_ids = caps.get_identities(dedupe=False) deduped_ids = caps.get_identities() if len(full_ids) != len(deduped_ids): log.debug("Duplicate disco identities found, invalid for caps") return False # Check Features full_features = caps.get_features(dedupe=False) deduped_features = caps.get_features() if len(full_features) != len(deduped_features): log.debug("Duplicate disco features found, invalid for caps") return False # Check Forms form_types = [] deduped_form_types = set() for stanza in caps['substanzas']: if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): log.debug("Non form extension found, ignoring for caps") caps.xml.remove(stanza.xml) continue if 'FORM_TYPE' in stanza.get_fields(): f_type = tuple(stanza.get_fields()['FORM_TYPE']['value']) form_types.append(f_type) deduped_form_types.add(f_type) if len(form_types) != len(deduped_form_types): log.debug("Duplicated FORM_TYPE values, " + \ "invalid for caps") return False if len(f_type) > 1: deduped_type = set(f_type) if len(f_type) != len(deduped_type): log.debug("Extra FORM_TYPE data, invalid for caps") return False if stanza.get_fields()['FORM_TYPE']['type'] != 'hidden': log.debug("Field FORM_TYPE type not 'hidden', " + \ "ignoring form for caps") caps.xml.remove(stanza.xml) else: log.debug("No FORM_TYPE found, ignoring form for caps") caps.xml.remove(stanza.xml) verstring = self.generate_verstring(caps, hash) if verstring != check_verstring: log.debug("Verification strings do not match: %s, %s" % ( verstring, check_verstring)) return False await self.cache_caps(verstring, caps) return True def generate_verstring(self, info, hash): hash = self.hashes.get(hash, None) if hash is None: return None S = '' # Convert None to '' in the identities def clean_identity(id): return map(lambda i: i or '', id) identities = map(clean_identity, info['identities']) identities = sorted(('/'.join(i) for i in identities)) features = sorted(info['features']) S += '<'.join(identities) + '<' S += '<'.join(features) + '<' form_types = {} for stanza in info['substanzas']: if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): if 'FORM_TYPE' in stanza.get_fields(): f_type = stanza['values']['FORM_TYPE'] if len(f_type): f_type = f_type[0] if f_type not in form_types: form_types[f_type] = [] form_types[f_type].append(stanza) sorted_forms = sorted(form_types.keys()) for f_type in sorted_forms: for form in form_types[f_type]: S += '%s<' % f_type fields = sorted(form.get_fields().keys()) fields.remove('FORM_TYPE') for field in fields: S += '%s<' % field vals = form.get_fields()[field].get_value(convert=False) if vals is None: S += '<' else: if not isinstance(vals, list): vals = [vals] S += '<'.join(sorted(vals)) + '<' binary = hash(S.encode('utf8')).digest() return base64.b64encode(binary).decode('utf-8') async def update_caps(self, jid: OptJidStr = None, node: Optional[str] = None, preserve: bool = False, broadcast: bool = True): """Update caps for a local JID based on current data. :param jid: JID whose info to update :param node: Node to fetch info from :param broadcast: Send a presence after updating. :param preserve: Send presence only to contacts found in the roster. """ try: info = await self.xmpp['xep_0030'].get_info(jid, node, local=True) if isinstance(info, Iq): info = info['disco_info'] ver = self.generate_verstring(info, self.hash) await self.xmpp['xep_0030'].set_info( jid=jid, node='%s#%s' % (self.caps_node, ver), info=info ) await self.cache_caps(ver, info) await self.assign_verstring(jid, ver) if broadcast and self.xmpp.sessionstarted and self.broadcast: if self.xmpp.is_component or preserve: for contact in self.xmpp.roster[jid]: self.xmpp.roster[jid][contact].send_last_presence() else: self.xmpp.roster[jid].send_last_presence() except XMPPError: return def get_verstring(self, jid=None) -> Future: """Get the stored verstring for a JID. .. versionchanged:: 1.8.0 This function now returns a Future. """ if jid in ('', None): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full return self.api['get_verstring'](jid) def assign_verstring(self, jid=None, verstring=None) -> Future: """Assign a vertification string to a jid. .. versionchanged:: 1.8.0 This function now returns a Future. """ if jid in (None, ''): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full return self.api['assign_verstring'](jid, args={ 'verstring': verstring }) def cache_caps(self, verstring=None, info=None) -> Future: """Add caps to the cache. .. versionchanged:: 1.8.0 This function now returns a Future. """ data = {'verstring': verstring, 'info': info} return self.api['cache_caps'](args=data) async def get_caps(self, jid=None, verstring=None): """Get caps for a JID. .. versionchanged:: 1.8.0 This function is now a coroutine. """ if verstring is None: if jid is not None: verstring = await self.get_verstring(jid) else: return None if isinstance(jid, JID): jid = jid.full data = {'verstring': verstring} return await self.api['get_caps'](jid, args=data) slixmpp/slixmpp/plugins/xep_0115/stanza.py000066400000000000000000000006571477105560000210600ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from __future__ import unicode_literals from slixmpp.xmlstream import ElementBase class Capabilities(ElementBase): namespace = 'http://jabber.org/protocol/caps' name = 'c' plugin_attrib = 'caps' interfaces = {'hash', 'node', 'ver', 'ext'} slixmpp/slixmpp/plugins/xep_0115/static.py000066400000000000000000000122531477105560000210420ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.xmlstream import JID from slixmpp.exceptions import IqError, IqTimeout log = logging.getLogger(__name__) class StaticCaps(object): """ Extend the default StaticDisco implementation to provide support for extended identity information. """ def __init__(self, xmpp, static): """ Augment the default XEP-0030 static handler object. Arguments: static -- The default static XEP-0030 handler object. """ self.xmpp = xmpp self.disco = self.xmpp['xep_0030'] self.caps = self.xmpp['xep_0115'] self.static = static self.jid_vers = {} async def supports(self, jid, node, ifrom, data): """ Check if a JID supports a given feature. The data parameter may provide: feature -- The feature to check for support. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ feature = data.get('feature', None) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} if not feature: return False if node in (None, ''): info = await self.caps.get_caps(jid) if info and feature in info['features']: return True try: info = await self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) return feature in info['disco_info']['features'] except IqError: return False except IqTimeout: return None async def has_identity(self, jid, node, ifrom, data): """ Check if a JID has a given identity. The data parameter may provide: category -- The category of the identity to check. itype -- The type of the identity to check. lang -- The language of the identity to check. local -- If true, then the query is for a JID/node combination handled by this Slixmpp instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. cached -- If true, then look for the disco info data from the local cache system. If no results are found, send the query as usual. The self.use_cache setting must be set to true for this option to be useful. If set to false, then the cache will be skipped, even if a result has already been cached. Defaults to false. """ identity = (data.get('category', None), data.get('itype', None), data.get('lang', None)) data = {'local': data.get('local', False), 'cached': data.get('cached', True)} trunc = lambda i: (i[0], i[1], i[2]) if node in (None, ''): info = self.caps.get_caps(jid) if info and identity in map(trunc, info['identities']): return True try: info = await self.disco.get_info(jid=jid, node=node, ifrom=ifrom, **data) info = self.disco._wrap(ifrom, jid, info, True) return identity in map(trunc, info['disco_info']['identities']) except IqError: return False except IqTimeout: return None def cache_caps(self, jid, node, ifrom, data): verstring = data.get('verstring', None) info = data.get('info', None) if not verstring or not info: return self.caps.cache.store(verstring, info) def assign_verstring(self, jid, node, ifrom, data): if isinstance(jid, JID): jid = jid.full self.jid_vers[jid] = data.get('verstring', None) def get_verstring(self, jid, node, ifrom, data): return self.jid_vers.get(jid, None) async def get_caps(self, jid, node, ifrom, data): verstring = data.get('verstring', None) if verstring is None: return None return self.caps.cache.retrieve(verstring) slixmpp/slixmpp/plugins/xep_0118/000077500000000000000000000000001477105560000172015ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0118/__init__.py000066400000000000000000000006231477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0118 import stanza from slixmpp.plugins.xep_0118.stanza import UserTune from slixmpp.plugins.xep_0118.user_tune import XEP_0118 register_plugin(XEP_0118) slixmpp/slixmpp/plugins/xep_0118/stanza.py000066400000000000000000000012041477105560000210500ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class UserTune(ElementBase): name = 'tune' namespace = 'http://jabber.org/protocol/tune' plugin_attrib = 'tune' interfaces = {'artist', 'length', 'rating', 'source', 'title', 'track', 'uri'} sub_interfaces = interfaces def set_length(self, value): self._set_sub_text('length', str(value)) def set_rating(self, value): self._set_sub_text('rating', str(value)) slixmpp/slixmpp/plugins/xep_0118/user_tune.py000066400000000000000000000044741477105560000215750ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0118 import stanza, UserTune log = logging.getLogger(__name__) class XEP_0118(BasePlugin): """ XEP-0118: User Tune """ name = 'xep_0118' description = 'XEP-0118: User Tune' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserTune.namespace) self.xmpp['xep_0163'].remove_interest(UserTune.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_tune', UserTune) def publish_tune(self, *, artist: Optional[str] = None, length: Optional[int] =None, rating: Optional[int] = None, source: Optional[str] = None, title: Optional[str] = None, track: Optional[str] = None, uri: Optional[str] = None, **pubsubkwargs) -> Future: """ Publish the user's current tune. :param artist: The artist or performer of the song. :param length: The length of the song in seconds. :param rating: The user's rating of the song (from 1 to 10) :param source: The album name, website, or other source of the song. :param title: The title of the song. :param track: The song's track number, or other unique identifier. :param uri: A URL to more information about the song. """ tune = UserTune() tune['artist'] = artist tune['length'] = length tune['rating'] = rating tune['source'] = source tune['title'] = title tune['track'] = track tune['uri'] = uri return self.xmpp['xep_0163'].publish( tune, node=UserTune.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user tune information to stop notifications. """ tune = UserTune() return self.xmpp['xep_0163'].publish( tune, node=UserTune.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0122/000077500000000000000000000000001477105560000171745ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0122/__init__.py000066400000000000000000000003071477105560000213050ustar00rootroot00000000000000 from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0122.stanza import FormValidation from slixmpp.plugins.xep_0122.data_validation import XEP_0122 register_plugin(XEP_0122) slixmpp/slixmpp/plugins/xep_0122/data_validation.py000066400000000000000000000010251477105560000226670ustar00rootroot00000000000000from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import stanza from slixmpp.plugins.xep_0004.stanza import FormField from slixmpp.plugins.xep_0122.stanza import FormValidation class XEP_0122(BasePlugin): """ XEP-0122: Data Forms """ name = 'xep_0122' description = 'XEP-0122: Data Forms Validation' dependencies = {'xep_0004'} stanza = stanza def plugin_init(self): register_stanza_plugin(FormField, FormValidation) slixmpp/slixmpp/plugins/xep_0122/stanza.py000066400000000000000000000053661477105560000210600ustar00rootroot00000000000000from slixmpp.xmlstream import ElementBase, ET class FormValidation(ElementBase): """ Validation values for form fields. Example: :: 2003-10-06T11:22:00-07:00 Questions: * Should this look at the datatype value and convert the range values as appropriate? * Should this stanza provide a pass/fail for a value from the field, or convert field value to datatype? """ namespace = 'http://jabber.org/protocol/xdata-validate' name = 'validate' plugin_attrib = 'validate' interfaces = {'datatype', 'basic', 'open', 'range', 'regex', } sub_interfaces = {'basic', 'open', 'range', 'regex', } plugin_attrib_map = {} plugin_tag_map = {} def _add_field(self, name): self.remove_all() item_xml = ET.Element('{%s}%s' % (self.namespace, name)) self.xml.append(item_xml) return item_xml def set_basic(self, value): if value: self._add_field('basic') else: del self['basic'] def set_open(self, value): if value: self._add_field('open') else: del self['open'] def set_regex(self, regex): if regex: _regex = self._add_field('regex') _regex.text = regex else: del self['regex'] def set_range(self, value, minimum=None, maximum=None): if value: _range = self._add_field('range') _range.attrib['min'] = str(minimum) _range.attrib['max'] = str(maximum) else: del self['range'] def remove_all(self, except_tag=None): for a in self.sub_interfaces: if a != except_tag: del self[a] def get_basic(self): present = self.xml.find('{%s}basic' % self.namespace) return present is not None def get_open(self): present = self.xml.find('{%s}open' % self.namespace) return present is not None def get_regex(self): present = self.xml.find('{%s}regex' % self.namespace) if present is not None: return present.text return False def get_range(self): present = self.xml.find('{%s}range' % self.namespace) if present is not None: attributes = present.attrib return_value = dict() if 'min' in attributes: return_value['minimum'] = attributes['min'] if 'max' in attributes: return_value['maximum'] = attributes['max'] return return_value return False slixmpp/slixmpp/plugins/xep_0128/000077500000000000000000000000001477105560000172025ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0128/__init__.py000066400000000000000000000005671477105560000213230ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0128.static import StaticExtendedDisco from slixmpp.plugins.xep_0128.extended_disco import XEP_0128 register_plugin(XEP_0128) slixmpp/slixmpp/plugins/xep_0128/extended_disco.py000066400000000000000000000063451477105560000225450ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional import slixmpp from slixmpp import Iq, JID from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0004 import Form from slixmpp.plugins.xep_0030 import DiscoInfo from slixmpp.plugins.xep_0128 import StaticExtendedDisco class XEP_0128(BasePlugin): """ XEP-0128: Service Discovery Extensions Allow the use of data forms to add additional identity information to disco#info results. Also see . :var disco: A reference to the XEP-0030 plugin. :var static: Object containing the default set of static node handlers. """ name = 'xep_0128' description = 'XEP-0128: Service Discovery Extensions' dependencies = {'xep_0030', 'xep_0004'} def plugin_init(self): """Start the XEP-0128 plugin.""" self._disco_ops = ['set_extended_info', 'add_extended_info', 'del_extended_info'] register_stanza_plugin(DiscoInfo, Form, iterable=True) self.disco = self.xmpp['xep_0030'] self.static = StaticExtendedDisco(self.disco.static) self.disco.set_extended_info = self.set_extended_info self.disco.add_extended_info = self.add_extended_info self.disco.del_extended_info = self.del_extended_info for op in self._disco_ops: self.api.register(getattr(self.static, op), op, default=True) def set_extended_info(self, jid=None, node=None, **kwargs) -> Future: """ Set additional, extended identity information to a node. Replaces any existing extended information. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param data: Either a form, or a list of forms to use as extended information, replacing any existing extensions. """ return self.api['set_extended_info'](jid, node, None, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs) -> Future: """ Add additional, extended identity information to a node. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. :param data: Either a form, or a list of forms to add as extended information. """ return self.api['add_extended_info'](jid, node, None, kwargs) def del_extended_info(self, jid: Optional[JID] = None, node: Optional[str] = None, **kwargs) -> Future: """ Remove all extended identity information to a node. .. versionchanged:: 1.8.0 This function now returns a Future. :param jid: The JID to modify. :param node: The node to modify. """ return self.api['del_extended_info'](jid, node, None, kwargs) slixmpp/slixmpp/plugins/xep_0128/static.py000066400000000000000000000036631477105560000210530ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging import slixmpp from slixmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) class StaticExtendedDisco(object): """ Extend the default StaticDisco implementation to provide support for extended identity information. """ def __init__(self, static): """ Augment the default XEP-0030 static handler object. Arguments: static -- The default static XEP-0030 handler object. """ self.static = static def set_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.del_extended_info(jid, node, ifrom, data) self.add_extended_info(jid, node, ifrom, data) def add_extended_info(self, jid, node, ifrom, data): """ Add additional extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.static.add_node(jid, node) forms = data.get('data', []) if not isinstance(forms, list): forms = [forms] info = self.static.get_node(jid, node)['info'] for form in forms: info.append(form) def del_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter is not used. """ if self.static.node_exists(jid, node): info = self.static.get_node(jid, node)['info'] for form in info['substanza']: info.xml.remove(form.xml) slixmpp/slixmpp/plugins/xep_0131/000077500000000000000000000000001477105560000171745ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0131/__init__.py000066400000000000000000000006201477105560000213030ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0131 import stanza from slixmpp.plugins.xep_0131.stanza import Headers from slixmpp.plugins.xep_0131.headers import XEP_0131 register_plugin(XEP_0131) slixmpp/slixmpp/plugins/xep_0131/headers.py000066400000000000000000000023741477105560000211670ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp import Message, Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0131 import stanza from slixmpp.plugins.xep_0131.stanza import Headers class XEP_0131(BasePlugin): name = 'xep_0131' description = 'XEP-0131: Stanza Headers and Internet Metadata' dependencies = {'xep_0030'} stanza = stanza default_config = { 'supported_headers': set() } def plugin_init(self): register_stanza_plugin(Message, Headers) register_stanza_plugin(Presence, Headers) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Headers.namespace) for header in self.supported_headers: self.xmpp['xep_0030'].del_feature( feature='%s#%s' % (Headers.namespace, header)) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Headers.namespace) for header in self.supported_headers: self.xmpp['xep_0030'].add_feature('%s#%s' % ( Headers.namespace, header)) slixmpp/slixmpp/plugins/xep_0131/stanza.py000066400000000000000000000027731477105560000210570ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ET, ElementBase class Headers(ElementBase): name = 'headers' namespace = 'http://jabber.org/protocol/shim' plugin_attrib = 'headers' interfaces = {'headers'} is_extension = True def get_headers(self): result = {} headers = self.xml.findall('{%s}header' % self.namespace) for header in headers: name = header.attrib.get('name', '') value = header.text if name in result: if not isinstance(result[name], set): result[name] = [result[name]] else: result[name] = [] result[name].add(value) else: result[name] = value return result def set_headers(self, values): self.del_headers() for name in values: vals = values[name] if not isinstance(vals, (list, set)): vals = [values[name]] for value in vals: header = ET.Element('{%s}header' % self.namespace) header.attrib['name'] = name header.text = value self.xml.append(header) def del_headers(self): headers = self.xml.findall('{%s}header' % self.namespace) for header in headers: self.xml.remove(header) slixmpp/slixmpp/plugins/xep_0133.py000066400000000000000000000035311477105560000175520ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins import BasePlugin, register_plugin class XEP_0133(BasePlugin): name = 'xep_0133' description = 'XEP-0133: Service Administration' dependencies = {'xep_0030', 'xep_0004', 'xep_0050'} commands = {'add-user', 'delete-user', 'disable-user', 'reenable-user', 'end-user-session', 'get-user-password', 'change-user-password', 'get-user-roster', 'get-user-lastlogin', 'user-stats', 'edit-blacklist', 'edit-whitelist', 'get-registered-users-num', 'get-disabled-users-num', 'get-online-users-num', 'get-active-users-num', 'get-idle-users-num', 'get-registered-users-list', 'get-disabled-users-list', 'get-online-users-list', 'get-online-users', 'get-active-users', 'get-idle-userslist', 'announce', 'set-motd', 'edit-motd', 'delete-motd', 'set-welcome', 'delete-welcome', 'edit-admin', 'restart', 'shutdown'} def get_commands(self, jid=None, **kwargs): if jid is None: jid = self.xmpp.boundjid.server return self.xmpp['xep_0050'].get_commands(jid, **kwargs) def create_command(name): def admin_command(self, jid=None, session=None, ifrom=None): if jid is None: jid = self.xmpp.boundjid.server self.xmpp['xep_0050'].start_command( jid=jid, node='http://jabber.org/protocol/admin#%s' % name, session=session, ifrom=ifrom) return admin_command for cmd in XEP_0133.commands: setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd)) register_plugin(XEP_0133) slixmpp/slixmpp/plugins/xep_0152/000077500000000000000000000000001477105560000171775ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0152/__init__.py000066400000000000000000000006321477105560000213110ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0152 import stanza from slixmpp.plugins.xep_0152.stanza import Reachability from slixmpp.plugins.xep_0152.reachability import XEP_0152 register_plugin(XEP_0152) slixmpp/slixmpp/plugins/xep_0152/reachability.py000066400000000000000000000041641477105560000222160ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from slixmpp import JID from typing import Dict, List, Optional, Callable from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0152 import stanza, Reachability from slixmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) class XEP_0152(BasePlugin): """ XEP-0152: Reachability Addresses """ name = 'xep_0152' description = 'XEP-0152: Reachability Addresses' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace) self.xmpp['xep_0163'].remove_interest(Reachability.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('reachability', Reachability) def publish_reachability(self, addresses: List[Dict[str, str]], **pubsubkwargs) -> Future: """ Publish alternative addresses where the user can be reached. :param addresses: A list of dictionaries containing the URI and optional description for each address. """ if not isinstance(addresses, (list, tuple)): addresses = [addresses] reach = Reachability() for address in addresses: if not hasattr(address, 'items'): address = {'uri': address} addr = stanza.Address() for key, val in address.items(): addr[key] = val reach.append(addr) return self.xmpp['xep_0163'].publish( reach, node=Reachability.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user activity information to stop notifications. """ reach = Reachability() return self.xmpp['xep_0163'].publish( reach, node=Reachability.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0152/stanza.py000066400000000000000000000012561477105560000210550ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, register_stanza_plugin class Reachability(ElementBase): name = 'reach' namespace = 'urn:xmpp:reach:0' plugin_attrib = 'reach' interfaces = set() class Address(ElementBase): name = 'addr' namespace = 'urn:xmpp:reach:0' plugin_attrib = 'address' plugin_multi_attrib = 'addresses' interfaces = {'uri', 'desc'} lang_interfaces = {'desc'} sub_interfaces = {'desc'} register_stanza_plugin(Reachability, Address, iterable=True) slixmpp/slixmpp/plugins/xep_0153/000077500000000000000000000000001477105560000172005ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0153/__init__.py000066400000000000000000000005611477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0153.stanza import VCardTempUpdate from slixmpp.plugins.xep_0153.vcard_avatar import XEP_0153 register_plugin(XEP_0153) slixmpp/slixmpp/plugins/xep_0153/stanza.py000066400000000000000000000013401477105560000210500ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase class VCardTempUpdate(ElementBase): name = 'x' namespace = 'vcard-temp:x:update' plugin_attrib = 'vcard_temp_update' interfaces = {'photo'} sub_interfaces = interfaces def set_photo(self, value): if value is not None: self._set_sub_text('photo', value, keep=True) else: self._del_sub('photo') def get_photo(self): photo = self.xml.find('{%s}photo' % self.namespace) if photo is None: return None return photo.text slixmpp/slixmpp/plugins/xep_0153/vcard_avatar.py000066400000000000000000000141041477105560000222070ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import hashlib import logging from asyncio import Future from typing import ( Dict, Optional, ) from slixmpp import JID from slixmpp.stanza import Presence from slixmpp.exceptions import XMPPError, IqTimeout, IqError from slixmpp.xmlstream import register_stanza_plugin, ElementBase from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0153 import stanza, VCardTempUpdate log = logging.getLogger(__name__) class XEP_0153(BasePlugin): name = 'xep_0153' description = 'XEP-0153: vCard-Based Avatars' dependencies = {'xep_0054'} stanza = stanza def plugin_init(self): self._hashes = {} register_stanza_plugin(Presence, VCardTempUpdate) self.xmpp.add_filter('out', self._update_presence) self.xmpp.add_event_handler('session_start', self._start) self.xmpp.add_event_handler('presence_available', self._recv_presence) self.xmpp.add_event_handler('presence_dnd', self._recv_presence) self.xmpp.add_event_handler('presence_xa', self._recv_presence) self.xmpp.add_event_handler('presence_chat', self._recv_presence) self.xmpp.add_event_handler('presence_away', self._recv_presence) self.api.register(self._set_hash, 'set_hash', default=True) self.api.register(self._get_hash, 'get_hash', default=True) self.api.register(self._reset_hash, 'reset_hash', default=True) def plugin_end(self): self.xmpp.del_filter('out', self._update_presence) self.xmpp.del_event_handler('session_start', self._start) self.xmpp.del_event_handler('session_end', self._end) self.xmpp.del_event_handler('presence_available', self._recv_presence) self.xmpp.del_event_handler('presence_dnd', self._recv_presence) self.xmpp.del_event_handler('presence_xa', self._recv_presence) self.xmpp.del_event_handler('presence_chat', self._recv_presence) self.xmpp.del_event_handler('presence_away', self._recv_presence) def set_avatar(self, jid: Optional[JID] = None, avatar: Optional[bytes] = None, mtype: Optional[str] = None, **iqkwargs) -> Future: """Set a VCard avatar. :param jid: The JID to set the avatar for. :param avatar: Avatar content. :param mtype: Avatar file type (e.g. image/jpeg). """ if jid is None: jid = self.xmpp.boundjid.bare async def get_and_set_avatar(): timeout = iqkwargs.get('timeout', None) try: result = await self.xmpp['xep_0054'].get_vcard( jid, cached=False, timeout=timeout ) except IqTimeout: raise vcard = result['vcard_temp'] vcard['PHOTO']['TYPE'] = mtype vcard['PHOTO']['BINVAL'] = avatar try: result = await self.xmpp['xep_0054'].publish_vcard( jid=jid, vcard=vcard, **iqkwargs ) except IqTimeout: raise await self.api['reset_hash'](jid) self.xmpp.roster[jid].send_last_presence() return self.xmpp.wrap(get_and_set_avatar()) async def _start(self, event): try: vcard = await self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare) data = vcard['vcard_temp']['PHOTO']['BINVAL'] if not data: new_hash = '' else: new_hash = hashlib.sha1(data).hexdigest() await self.api['set_hash'](self.xmpp.boundjid, args=new_hash) except XMPPError: log.debug('Could not retrieve vCard for %s', self.xmpp.boundjid.bare) async def _update_presence(self, stanza: ElementBase) -> ElementBase: if not isinstance(stanza, Presence): return stanza if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'): return stanza current_hash = await self.api['get_hash'](stanza['from']) stanza['vcard_temp_update']['photo'] = current_hash return stanza async def _recv_presence(self, pres: Presence): try: if pres.get_plugin('muc', check=True): # Don't process vCard avatars for MUC occupants # since they all share the same bare JID. return except: pass if not pres.match('presence/vcard_temp_update'): await self.api['set_hash'](pres['from'], args=None) return data = pres['vcard_temp_update']['photo'] if data is None: return self.xmpp.event('vcard_avatar_update', pres) # ================================================================= async def _reset_hash(self, jid: JID, node: str, ifrom: JID, args: Dict): own_jid = (jid.bare == self.xmpp.boundjid.bare) if self.xmpp.is_component: own_jid = (jid.domain == self.xmpp.boundjid.domain) await self.api['set_hash'](jid, args=None) if own_jid: self.xmpp.roster[jid].send_last_presence() try: iq = await self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom) except (IqError, IqTimeout): log.debug('Could not retrieve vCard for %s', jid) return try: data = iq['vcard_temp']['PHOTO']['BINVAL'] except ValueError: log.debug('Invalid BINVAL in vCard’s PHOTO for %s:', jid, exc_info=True) data = None if not data: new_hash = '' else: new_hash = hashlib.sha1(data).hexdigest() await self.api['set_hash'](jid, args=new_hash) def _get_hash(self, jid: JID, node: str, ifrom: JID, args: Dict): return self._hashes.get(jid.bare, None) def _set_hash(self, jid: JID, node: str, ifrom: JID, args: Dict): self._hashes[jid.bare] = args slixmpp/slixmpp/plugins/xep_0163.py000066400000000000000000000106071477105560000175570ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging from typing import Optional, Callable from slixmpp import JID from slixmpp.xmlstream import register_stanza_plugin, ElementBase from slixmpp.plugins.base import BasePlugin, register_plugin from slixmpp.plugins.xep_0004.stanza import Form log = logging.getLogger(__name__) class XEP_0163(BasePlugin): """ XEP-0163: Personal Eventing Protocol (PEP) """ name = 'xep_0163' description = 'XEP-0163: Personal Eventing Protocol (PEP)' dependencies = {'xep_0030', 'xep_0060', 'xep_0115'} def register_pep(self, name, stanza): """ Setup and configure events and stanza registration for the given PEP stanza: - Add disco feature for the PEP content. - Register disco interest in the PEP content. - Map events from the PEP content's namespace to the given name. :param str name: The event name prefix to use for PEP events. :param stanza: The stanza class for the PEP content. """ pubsub_stanza = self.xmpp['xep_0060'].stanza register_stanza_plugin(pubsub_stanza.EventItem, stanza) self.add_interest(stanza.namespace) self.xmpp['xep_0030'].add_feature(stanza.namespace) self.xmpp['xep_0060'].map_node_event(stanza.namespace, name) def add_interest(self, namespace: str, jid: Optional[JID] = None): """ Mark an interest in a PEP subscription by including a disco feature with the '+notify' extension. :param namespace: The base namespace to register as an interest, such as 'http://jabber.org/protocol/tune'. This may also be a list of such namespaces. :param jid: Optionally specify the JID. """ if not isinstance(namespace, set) and not isinstance(namespace, list): namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].add_feature('%s+notify' % ns, jid=jid) asyncio.ensure_future( self.xmpp['xep_0115'].update_caps(jid, broadcast=False), loop=self.xmpp.loop, ) def remove_interest(self, namespace: str, jid: Optional[JID] = None): """ Mark an interest in a PEP subscription by including a disco feature with the '+notify' extension. :param namespace: The base namespace to remove as an interest, such as 'http://jabber.org/protocol/tune'. This may also be a list of such namespaces. :param jid: Optionally specify the JID. """ if not isinstance(namespace, (set, list)): namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].del_feature(jid=jid, feature='%s+notify' % namespace) asyncio.ensure_future( self.xmpp['xep_0115'].update_caps(jid, broadcast=False), loop=self.xmpp.loop, ) def publish(self, stanza: ElementBase, node: Optional[str] = None, id: Optional[str] = None, options: Optional[Form] = None, ifrom: Optional[JID] = None, callback: Optional[Callable] = None, timeout: Optional[int] = None): """ Publish a PEP update. This is just a (very) thin wrapper around the XEP-0060 publish() method to set the defaults expected by PEP. :param stanza: The PEP update stanza to publish. :param node: The node to publish the item to. If not specified, the stanza's namespace will be used. :param id: Optionally specify the ID of the item. :param options: A form of publish options. """ if node is None: node = stanza.namespace if id is None: id = 'current' return self.xmpp['xep_0060'].publish(ifrom, node, id=id, payload=stanza.xml, options=options, ifrom=ifrom, callback=callback, timeout=timeout) register_plugin(XEP_0163) slixmpp/slixmpp/plugins/xep_0172/000077500000000000000000000000001477105560000172015ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0172/__init__.py000066400000000000000000000006231477105560000213130ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0172 import stanza from slixmpp.plugins.xep_0172.stanza import UserNick from slixmpp.plugins.xep_0172.user_nick import XEP_0172 register_plugin(XEP_0172) slixmpp/slixmpp/plugins/xep_0172/stanza.py000066400000000000000000000034331477105560000210560ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class UserNick(ElementBase): """ XEP-0172: User Nickname allows the addition of a element in several stanza types, including and stanzas. The nickname contained in a should be the global, friendly or informal name chosen by the owner of a bare JID. The element may be included when establishing communications with new entities, such as normal XMPP users or MUC services. The nickname contained in a element will not necessarily be the same as the nickname used in a MUC. Example stanzas: :: The User ... The User Stanza Interface: :: nick -- A global, friendly or informal name chosen by a user. """ namespace = 'http://jabber.org/protocol/nick' name = 'nick' plugin_attrib = name interfaces = {'nick'} def set_nick(self, nick): """ Add a element with the given nickname. Arguments: nick -- A human readable, informal name. """ self.xml.text = nick def get_nick(self): """Return the nickname in the element.""" return self.xml.text def del_nick(self): """Remove the element.""" if self.parent is not None: self.parent().xml.remove(self.xml) slixmpp/slixmpp/plugins/xep_0172/user_nick.py000066400000000000000000000037061477105560000215430ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import Optional, Callable from slixmpp import JID from slixmpp.stanza.message import Message from slixmpp.stanza.presence import Presence from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0172 import stanza, UserNick from slixmpp.plugins.xep_0004.stanza import Form log = logging.getLogger(__name__) class XEP_0172(BasePlugin): """ XEP-0172: User Nickname """ name = 'xep_0172' description = 'XEP-0172: User Nickname' dependencies = {'xep_0163'} stanza = stanza def plugin_init(self): register_stanza_plugin(Message, UserNick) register_stanza_plugin(Presence, UserNick) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserNick.namespace) self.xmpp['xep_0163'].remove_interest(UserNick.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_nick', UserNick) def publish_nick(self, nick: Optional[str] = None, **pubsubkwargs) -> Future: """ Publish the user's current nick. :param nick: The user nickname to publish. """ nickname = UserNick() nickname['nick'] = nick return self.xmpp['xep_0163'].publish( nickname, node=UserNick.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user nick information to stop notifications. """ nick = UserNick() return self.xmpp['xep_0163'].publish( nick, node=UserNick.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0184/000077500000000000000000000000001477105560000172045ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0184/__init__.py000066400000000000000000000005651477105560000213230ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0184.stanza import Request, Received from slixmpp.plugins.xep_0184.receipt import XEP_0184 register_plugin(XEP_0184) slixmpp/slixmpp/plugins/xep_0184/receipt.py000066400000000000000000000074261477105560000212220ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp.stanza import Message from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0184 import stanza, Request, Received class XEP_0184(BasePlugin): """ XEP-0184: Message Delivery Receipts """ name = 'xep_0184' description = 'XEP-0184: Message Delivery Receipts' dependencies = {'xep_0030'} stanza = stanza default_config = { 'auto_ack': True, 'auto_request': False } ack_types = ('normal', 'chat', 'headline') def plugin_init(self): register_stanza_plugin(Message, Request) register_stanza_plugin(Message, Received) self.xmpp.add_filter('out', self._filter_add_receipt_request) self.xmpp.register_handler( Callback('Message Receipt', StanzaPath('message/receipt'), self._handle_receipt_received)) self.xmpp.register_handler( Callback('Message Receipt Request', StanzaPath('message/request_receipt'), self._handle_receipt_request)) def plugin_end(self): self.xmpp['xep_0030'].del_feature('urn:xmpp:receipts') self.xmpp.del_filter('out', self._filter_add_receipt_request) self.xmpp.remove_handler('Message Receipt') self.xmpp.remove_handler('Message Receipt Request') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts') def ack(self, msg): """ Acknowledge a message by sending a receipt. Arguments: msg -- The message to acknowledge. """ ack = self.xmpp.Message() ack['to'] = msg['from'] ack['receipt'] = msg['id'] ack.send() def _handle_receipt_received(self, msg): self.xmpp.event('receipt_received', msg) def _handle_receipt_request(self, msg): """ Auto-ack message receipt requests if ``self.auto_ack`` is ``True``. Arguments: msg -- The incoming message requesting a receipt. """ if self.auto_ack: if msg['type'] in self.ack_types: if not msg['receipt']: self.ack(msg) def _filter_add_receipt_request(self, stanza): """ Auto add receipt requests to outgoing messages, if: - ``self.auto_request`` is set to ``True`` - The message is not for groupchat - The message does not contain a receipt acknowledgment - The recipient is a bare JID or, if a full JID, one that has the ``urn:xmpp:receipts`` feature enabled The disco cache is checked if a full JID is specified in the outgoing message, which may mean a round-trip disco#info delay for the first message sent to the JID if entity caps are not used. """ if not self.auto_request: return stanza if not isinstance(stanza, Message): return stanza if stanza['request_receipt']: return stanza if not stanza['type'] in self.ack_types: return stanza if stanza['receipt']: return stanza if not stanza['body']: return stanza if stanza['to'].resource: if not self.xmpp['xep_0030'].supports(stanza['to'], feature='urn:xmpp:receipts', cached=True): return stanza stanza['request_receipt'] = True return stanza slixmpp/slixmpp/plugins/xep_0184/stanza.py000066400000000000000000000037301477105560000210610ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream.stanzabase import ElementBase, ET class Request(ElementBase): namespace = 'urn:xmpp:receipts' name = 'request' plugin_attrib = 'request_receipt' interfaces = {'request_receipt'} sub_interfaces = interfaces is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_request_receipt(self, val): self.del_request_receipt() if val: parent = self.parent() parent._set_sub_text("{%s}request" % self.namespace, keep=True) if not parent['id']: if parent.stream: parent['id'] = parent.stream.new_id() def get_request_receipt(self): parent = self.parent() if parent.xml.find("{%s}request" % self.namespace) is not None: return True else: return False def del_request_receipt(self): self.parent()._del_sub("{%s}request" % self.namespace) class Received(ElementBase): namespace = 'urn:xmpp:receipts' name = 'received' plugin_attrib = 'receipt' interfaces = {'receipt'} sub_interfaces = interfaces is_extension = True def setup(self, xml=None): self.xml = ET.Element('') return True def set_receipt(self, value): self.del_receipt() if value: parent = self.parent() xml = ET.Element("{%s}received" % self.namespace) xml.attrib['id'] = value parent.append(xml) def get_receipt(self): parent = self.parent() xml = parent.xml.find("{%s}received" % self.namespace) if xml is not None: return xml.attrib.get('id', '') return '' def del_receipt(self): self.parent()._del_sub('{%s}received' % self.namespace) slixmpp/slixmpp/plugins/xep_0186/000077500000000000000000000000001477105560000172065ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0186/__init__.py000066400000000000000000000006451477105560000213240ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0186 import stanza from slixmpp.plugins.xep_0186.stanza import Invisible, Visible from slixmpp.plugins.xep_0186.invisible_command import XEP_0186 register_plugin(XEP_0186) slixmpp/slixmpp/plugins/xep_0186/invisible_command.py000066400000000000000000000022461477105560000232460ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from slixmpp import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream import register_stanza_plugin from slixmpp.plugins.xep_0186 import stanza, Visible, Invisible log = logging.getLogger(__name__) class XEP_0186(BasePlugin): name = 'xep_0186' description = 'XEP-0186: Invisible Command' dependencies = {'xep_0030'} def plugin_init(self): register_stanza_plugin(Iq, Visible) register_stanza_plugin(Iq, Invisible) def set_invisible(self, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('invisible') return iq.send(callback=callback, timeout=timeout) def set_visible(self, ifrom=None, callback=None, timeout=None): iq = self.xmpp.Iq() iq['type'] = 'set' iq['from'] = ifrom iq.enable('visible') return iq.send(callback=callback, timeout=timeout) slixmpp/slixmpp/plugins/xep_0186/stanza.py000066400000000000000000000007741477105560000210700ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class Invisible(ElementBase): name = 'invisible' namespace = 'urn:xmpp:invisible:0' plugin_attrib = 'invisible' interfaces = set() class Visible(ElementBase): name = 'visible' namespace = 'urn:xmpp:visible:0' plugin_attrib = 'visible' interfaces = set() slixmpp/slixmpp/plugins/xep_0191/000077500000000000000000000000001477105560000172025ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0191/__init__.py000066400000000000000000000005671477105560000213230ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0191.stanza import Block, Unblock, BlockList from slixmpp.plugins.xep_0191.blocking import XEP_0191 register_plugin(XEP_0191) slixmpp/slixmpp/plugins/xep_0191/blocking.py000066400000000000000000000052461477105560000213530ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from typing import ( List, Optional, Set, Union, ) from slixmpp.stanza import Iq from slixmpp.plugins import BasePlugin from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.plugins.xep_0191 import stanza, Block, Unblock, BlockList log = logging.getLogger(__name__) BlockedJIDs = Union[ JID, Set[JID], List[JID] ] class XEP_0191(BasePlugin): name = 'xep_0191' description = 'XEP-0191: Blocking Command' dependencies = {'xep_0030'} stanza = stanza def plugin_init(self): register_stanza_plugin(Iq, BlockList) register_stanza_plugin(Iq, Block) register_stanza_plugin(Iq, Unblock) self.xmpp.register_handler( Callback('Blocked Contact', StanzaPath('iq@type=set/block'), self._handle_blocked)) self.xmpp.register_handler( Callback('Unblocked Contact', StanzaPath('iq@type=set/unblock'), self._handle_unblocked)) def plugin_end(self): self.xmpp.remove_handler('Blocked Contact') self.xmpp.remove_handler('Unblocked Contact') def get_blocked(self, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Get the list of blocked JIDs.""" iq = self.xmpp.make_iq_get(ifrom=ifrom) iq.enable('blocklist') return iq.send(**iqkwargs) def block(self, jids: BlockedJIDs, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Block a JID or a list of JIDs. :param jids: JID(s) to block. """ iq = self.xmpp.make_iq_set(ifrom=ifrom) if not isinstance(jids, (set, list)): jids = [jids] iq['block']['items'] = jids return iq.send(**iqkwargs) def unblock(self, jids: BlockedJIDs, ifrom: Optional[JID] = None, **iqkwargs) -> Future: """Unblock a JID or a list of JIDs. :param jids: JID(s) to unblock. """ if jids is None: raise ValueError("jids cannot be empty.") iq = self.xmpp.make_iq_set(ifrom=ifrom) if not isinstance(jids, (set, list)): jids = [jids] iq['unblock']['items'] = jids return iq.send(**iqkwargs) def _handle_blocked(self, iq): self.xmpp.event('blocked', iq) def _handle_unblocked(self, iq): self.xmpp.event('unblocked', iq) slixmpp/slixmpp/plugins/xep_0191/stanza.py000066400000000000000000000023761477105560000210640ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ET, ElementBase, JID class BlockList(ElementBase): name = 'blocklist' namespace = 'urn:xmpp:blocking' plugin_attrib = 'blocklist' interfaces = {'items'} def get_items(self): result = set() items = self.xml.findall('{%s}item' % self.namespace) if items is not None: for item in items: jid = JID(item.attrib.get('jid', '')) if jid: result.add(jid) return result def set_items(self, values): self.del_items() for jid in values: if jid: item = ET.Element('{%s}item' % self.namespace) item.attrib['jid'] = JID(jid).full self.xml.append(item) def del_items(self): items = self.xml.findall('{%s}item' % self.namespace) if items is not None: for item in items: self.xml.remove(item) class Block(BlockList): name = 'block' plugin_attrib = 'block' class Unblock(BlockList): name = 'unblock' plugin_attrib = 'unblock' slixmpp/slixmpp/plugins/xep_0196/000077500000000000000000000000001477105560000172075ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0196/__init__.py000066400000000000000000000006271477105560000213250ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0196 import stanza from slixmpp.plugins.xep_0196.stanza import UserGaming from slixmpp.plugins.xep_0196.user_gaming import XEP_0196 register_plugin(XEP_0196) slixmpp/slixmpp/plugins/xep_0196/stanza.py000066400000000000000000000007631477105560000210670ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.xmlstream import ElementBase, ET class UserGaming(ElementBase): 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 slixmpp/slixmpp/plugins/xep_0196/user_gaming.py000066400000000000000000000053721477105560000220700ustar00rootroot00000000000000# Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import logging from asyncio import Future from slixmpp import JID from typing import Optional, Callable from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0196 import stanza, UserGaming from slixmpp.plugins.xep_0004.stanza import Form log = logging.getLogger(__name__) class XEP_0196(BasePlugin): """ XEP-0196: User Gaming """ name = 'xep_0196' description = 'XEP-0196: User Gaming' dependencies = {'xep_0163'} stanza = stanza def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace) self.xmpp['xep_0163'].remove_interest(UserGaming.namespace) def session_bind(self, jid): self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming) def publish_gaming(self, name: Optional[str] = None, level: Optional[str] = None, server_name: Optional[str] = None, uri: Optional[str] = None, character_name: Optional[str] = None, character_profile: Optional[str] = None, server_address: Optional[str] = None, **pubsubkwargs) -> Future: """ Publish the user's current gaming status. :param name: The name of the game. :param level: The user's level in the game. :param uri: A URI for the game or relevant gaming service :param server_name: The name of the server where the user is playing. :param server_address: The hostname or IP address of the server where the user is playing. :param character_name: The name of the user's character in the game. :param character_profile: A URI for a profile of the user's character. :param options: Optional form of publish options. """ gaming = UserGaming() gaming['name'] = name gaming['level'] = level gaming['uri'] = uri gaming['character_name'] = character_name gaming['character_profile'] = character_profile gaming['server_name'] = server_name gaming['server_address'] = server_address return self.xmpp['xep_0163'].publish( gaming, node=UserGaming.namespace, **pubsubkwargs ) def stop(self, **pubsubkwargs) -> Future: """ Clear existing user gaming information to stop notifications. """ gaming = UserGaming() return self.xmpp['xep_0163'].publish( gaming, node=UserGaming.namespace, **pubsubkwargs ) slixmpp/slixmpp/plugins/xep_0198/000077500000000000000000000000001477105560000172115ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0198/__init__.py000066400000000000000000000011371477105560000213240ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0198.stanza import Enable, Enabled from slixmpp.plugins.xep_0198.stanza import Resume, Resumed from slixmpp.plugins.xep_0198.stanza import Failed from slixmpp.plugins.xep_0198.stanza import StreamManagement from slixmpp.plugins.xep_0198.stanza import Ack, RequestAck from slixmpp.plugins.xep_0198.stream_management import XEP_0198 register_plugin(XEP_0198) slixmpp/slixmpp/plugins/xep_0198/stanza.py000066400000000000000000000067051477105560000210730ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.stanza import Error from slixmpp.xmlstream import ElementBase, StanzaBase class Enable(StanzaBase): name = 'enable' namespace = 'urn:xmpp:sm:3' interfaces = {'max', 'resume'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_resume(self): return self._get_attr('resume', 'false').lower() in ('true', '1') def set_resume(self, val): self._del_attr('resume') self._set_attr('resume', 'true' if val else 'false') class Enabled(StanzaBase): name = 'enabled' namespace = 'urn:xmpp:sm:3' interfaces = {'id', 'location', 'max', 'resume'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_resume(self): return self._get_attr('resume', 'false').lower() in ('true', '1') def set_resume(self, val): self._del_attr('resume') self._set_attr('resume', 'true' if val else 'false') class Resume(StanzaBase): name = 'resume' namespace = 'urn:xmpp:sm:3' interfaces = {'h', 'previd'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) class Resumed(StanzaBase): name = 'resumed' namespace = 'urn:xmpp:sm:3' interfaces = {'h', 'previd'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) class Failed(StanzaBase, Error): name = 'failed' namespace = 'urn:xmpp:sm:3' interfaces = set() def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class StreamManagement(ElementBase): name = 'sm' namespace = 'urn:xmpp:sm:3' plugin_attrib = name interfaces = {'required', 'optional'} def get_required(self): return self.xml.find('{%s}required' % self.namespace) is not None def set_required(self, val): self.del_required() if val: self._set_sub_text('required', '', keep=True) def del_required(self): self._del_sub('required') def get_optional(self): return self.xml.find('{%s}optional' % self.namespace) is not None def set_optional(self, val): self.del_optional() if val: self._set_sub_text('optional', '', keep=True) def del_optional(self): self._del_sub('optional') class RequestAck(StanzaBase): name = 'r' namespace = 'urn:xmpp:sm:3' interfaces = set() def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() class Ack(StanzaBase): name = 'a' namespace = 'urn:xmpp:sm:3' interfaces = {'h'} def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_h(self): h = self._get_attr('h', None) if h: return int(h) return None def set_h(self, val): self._set_attr('h', str(val)) slixmpp/slixmpp/plugins/xep_0198/stream_management.py000066400000000000000000000277251477105560000232670ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import logging import collections from slixmpp.stanza import Message, Presence, Iq, StreamFeatures from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.handler import Callback, Waiter from slixmpp.xmlstream.matcher import MatchXPath, MatchMany from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0198 import stanza log = logging.getLogger(__name__) MAX_SEQ = 2 ** 32 class XEP_0198(BasePlugin): """ XEP-0198: Stream Management """ name = 'xep_0198' description = 'XEP-0198: Stream Management' dependencies = set() stanza = stanza default_config = { #: The last ack number received from the server. 'last_ack': 0, #: The number of stanzas to wait between sending ack requests to #: the server. Setting this to ``1`` will send an ack request after #: every sent stanza. Defaults to ``5``. 'window': 5, #: The stream management ID for the stream. Knowing this value is #: required in order to do stream resumption. 'sm_id': None, #: A counter of handled incoming stanzas, mod 2^32. 'handled': 0, #: A counter of unacked outgoing stanzas, mod 2^32. 'seq': 0, #: Control whether or not the ability to resume the stream will be #: requested when enabling stream management. Defaults to ``True``. 'allow_resume': True, 'order': 10100, 'resume_order': 9000 } def plugin_init(self): """Start the XEP-0198 plugin.""" # Only enable stream management for non-components, # since components do not yet perform feature negotiation. if self.xmpp.is_component: return self.window_counter = self.window self.enabled_in = False self.enabled_out = False self.unacked_queue = collections.deque() register_stanza_plugin(StreamFeatures, stanza.StreamManagement) self.xmpp.register_stanza(stanza.Enable) self.xmpp.register_stanza(stanza.Enabled) self.xmpp.register_stanza(stanza.Resume) self.xmpp.register_stanza(stanza.Resumed) self.xmpp.register_stanza(stanza.Ack) self.xmpp.register_stanza(stanza.RequestAck) # Register the feature twice because it may be ordered two # different ways: enabling after binding and resumption # before binding. self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, order=self.order) self.xmpp.register_feature('sm', self._handle_sm_feature, restart=True, order=self.resume_order) self.xmpp.register_handler( Callback('Stream Management Enabled', MatchXPath(stanza.Enabled.tag_name()), self._handle_enabled, instream=True)) self.xmpp.register_handler( Callback('Stream Management Resumed', MatchXPath(stanza.Resumed.tag_name()), self._handle_resumed, instream=True)) self.xmpp.register_handler( Callback('Stream Management Failed', MatchXPath(stanza.Failed.tag_name()), self._handle_failed, instream=True)) self.xmpp.register_handler( Callback('Stream Management Ack', MatchXPath(stanza.Ack.tag_name()), self._handle_ack, instream=True)) self.xmpp.register_handler( Callback('Stream Management Request Ack', MatchXPath(stanza.RequestAck.tag_name()), self._handle_request_ack, instream=True)) 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): if self.xmpp.is_component: return 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) self.xmpp.remove_handler('Stream Management Enabled') self.xmpp.remove_handler('Stream Management Resumed') self.xmpp.remove_handler('Stream Management Failed') self.xmpp.remove_handler('Stream Management Ack') self.xmpp.remove_handler('Stream Management Request Ack') self.xmpp.remove_stanza(stanza.Enable) self.xmpp.remove_stanza(stanza.Enabled) self.xmpp.remove_stanza(stanza.Resume) self.xmpp.remove_stanza(stanza.Resumed) 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.""" 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 self.seq = 0 self.last_ack = 0 def send_ack(self): """Send the current ack count to the server.""" if not self.xmpp.transport: log.debug('Disconnected: not sending ack') return ack = stanza.Ack(self.xmpp) ack['h'] = self.handled self.xmpp.send_raw(str(ack)) 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)) async def _handle_sm_feature(self, features): """ Enable or resume stream management. If no SM-ID is stored, and resource binding has taken place, stream management will be enabled. If an SM-ID is known, and the server allows resumption, the previous stream will be resumed. """ if 'stream_management' in self.xmpp.features: # We've already negotiated stream management, # so no need to do it again. return False if self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features: 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 # _handle_resumed() or _handle_failed() methods. waiter = Waiter('resumed_or_failed', MatchMany([ MatchXPath(stanza.Resumed.tag_name()), MatchXPath(stanza.Failed.tag_name())])) self.xmpp.register_handler(waiter) result = await waiter.wait() if result is not None and result.name == 'resumed': return True self.xmpp.event("session_end") if 'bind' in self.xmpp.features: enable = stanza.Enable(self.xmpp) enable['resume'] = self.allow_resume enable.send() log.debug("enabling SM") waiter = Waiter('enabled_or_failed', MatchMany([ MatchXPath(stanza.Enabled.tag_name()), MatchXPath(stanza.Failed.tag_name())])) self.xmpp.register_handler(waiter) result = await waiter.wait() return False def _handle_enabled(self, stanza): """Save the SM-ID, if provided. Raises an :term:`sm_enabled` event. """ 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. 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): """ Disable and reset any features used since stream management was requested (tracked stanzas may have been sent during the interval between the enable request and the enabled response). Raises an :term:`sm_failed` event. """ self.enabled_in = False self.enabled_out = False self.unacked_queue.clear() self.xmpp.event('sm_failed', stanza) def _handle_ack(self, ack): """Process a server ack by freeing acked stanzas from the queue. Raises a :term:`stanza_acked` event for each acked stanza. """ if ack['h'] == self.last_ack: return num_acked = (ack['h'] - self.last_ack) % MAX_SEQ num_unacked = len(self.unacked_queue) log.debug("Ack: %s, Last Ack: %s, " + \ "Unacked: %s, Num Acked: %s, " + \ "Remaining: %s", ack['h'], self.last_ack, num_unacked, num_acked, num_unacked - num_acked) if num_acked > len(self.unacked_queue) or num_acked < 0: log.error('Inconsistent sequence numbers from the server,' ' ignoring and replacing ours with them.') num_acked = len(self.unacked_queue) for x in range(num_acked): seq, stanza = self.unacked_queue.popleft() self.xmpp.event('stanza_acked', stanza) self.last_ack = ack['h'] def _handle_request_ack(self, req): """Handle an ack request by sending an ack.""" self.send_ack() def _handle_incoming(self, stanza): """Increment the handled counter for each inbound stanza.""" if not self.enabled_in: return stanza if isinstance(stanza, (Message, Presence, Iq)): # Sequence numbers are mod 2^32 self.handled = (self.handled + 1) % MAX_SEQ return stanza def _handle_outgoing(self, stanza): """Store outgoing stanzas in a queue to be acked.""" from slixmpp.plugins.xep_0198 import stanza as st if isinstance(stanza, (st.Enable, st.Resume)): self.enabled_out = True # do not clear the queue on resume if isinstance(stanza, st.Enable): self.unacked_queue.clear() log.debug("enabling outgoing SM: %s" % stanza) if not self.enabled_out: return stanza if isinstance(stanza, (Message, Presence, Iq)): seq = None # Sequence numbers are mod 2^32 self.seq = (self.seq + 1) % MAX_SEQ seq = self.seq self.unacked_queue.append((seq, stanza)) self.window_counter -= 1 if self.window_counter == 0: self.window_counter = self.window self.request_ack() return stanza slixmpp/slixmpp/plugins/xep_0199/000077500000000000000000000000001477105560000172125ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0199/__init__.py000066400000000000000000000005141477105560000213230ustar00rootroot00000000000000 # 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 slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0199.stanza import Ping from slixmpp.plugins.xep_0199.ping import XEP_0199 register_plugin(XEP_0199) slixmpp/slixmpp/plugins/xep_0199/ping.py000066400000000000000000000166301477105560000205270ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import asyncio import time import logging from asyncio import Future from typing import Optional, Callable, List from slixmpp.jid import JID from slixmpp.stanza import Iq from slixmpp.exceptions import IqError, IqTimeout from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.handler import Callback from slixmpp.plugins import BasePlugin from slixmpp.plugins.xep_0199 import stanza, Ping log = logging.getLogger(__name__) class XEP_0199(BasePlugin): """ XEP-0199: XMPP Ping Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Also see . Attributes: keepalive -- If True, periodically send ping requests to the server. If a ping is not answered, the connection will be reset. interval -- Time in seconds between keepalive pings. Defaults to 300 seconds. timeout -- Time in seconds to wait for a ping response. Defaults to 30 seconds. Methods: send_ping -- Send a ping to a given JID, returning the round trip time. """ name = 'xep_0199' description = 'XEP-0199: XMPP Ping' dependencies = {'xep_0030'} stanza = stanza default_config = { 'keepalive': False, 'interval': 300, 'timeout': 30 } def plugin_init(self): """ Start the XEP-0199 plugin. """ register_stanza_plugin(Iq, Ping) self.__pending_futures: List[Future] = [] self.xmpp.register_handler( Callback('Ping', StanzaPath('iq@type=get/ping'), self._handle_ping)) if self.keepalive: self.xmpp.add_event_handler('session_start', self.enable_keepalive) self.xmpp.add_event_handler('session_resumed', self.enable_keepalive) self.xmpp.add_event_handler('disconnected', self.disable_keepalive) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Ping.namespace) self.xmpp.remove_handler('Ping') if self.keepalive: self.xmpp.del_event_handler('session_start', self.enable_keepalive) self.xmpp.del_event_handler('session_resumed', self.enable_keepalive) self.xmpp.del_event_handler('disconnected', self.disable_keepalive) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Ping.namespace) def _clear_pending_futures(self): """Cancel all pending ping futures""" if self.__pending_futures: log.debug('Clearing %s pdnding pings', len(self.__pending_futures)) for future in self.__pending_futures: future.cancel() self.__pending_futures.clear() def enable_keepalive(self, interval: Optional[float] = None, timeout: Optional[float] = None) -> None: """ Enable the ping keepalive on the connection. The plugin will send a ping at `interval` and reconnect if the ping timeouts. :param interval: The interval between each ping :param timeout: The timeout of the ping """ if interval: self.interval = interval if timeout: self.timeout = timeout self.keepalive = True def handler(event=None): # Cleanup futures if self.__pending_futures: tmp_futures = [] for future in self.__pending_futures[:]: if not future.done(): tmp_futures.append(future) self.__pending_futures = tmp_futures future = asyncio.ensure_future( self._keepalive(event), loop=self.xmpp.loop, ) self.__pending_futures.append(future) self.xmpp.schedule('Ping keepalive', self.interval, handler, repeat=True) def disable_keepalive(self, event=None): self._clear_pending_futures() self.xmpp.cancel_schedule('Ping keepalive') session_end = disable_keepalive async def _keepalive(self, event=None): log.debug("Keepalive ping...") try: ifrom = None if self.xmpp.is_component: ifrom = self.xmpp.boundjid rtt = await self.ping( self.xmpp.boundjid.host, timeout=self.timeout, ifrom=ifrom ) except IqTimeout: log.debug("Did not receive ping back in time. " + "Requesting Reconnect.") self.xmpp.reconnect(0.0, "Ping timeout after %ds" % self.timeout) else: log.debug('Keepalive RTT: %s' % rtt) def _handle_ping(self, iq): """Automatically reply to ping requests.""" log.debug("Pinged by %s", iq['from']) iq.reply().send() def send_ping(self, jid: JID, ifrom: Optional[JID] = None, timeout: Optional[float] = None, callback: Optional[Callable] = None) -> Future[Iq]: """Send a ping request. :param jid: The JID that will receive the ping. """ if not timeout: timeout = self.timeout iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid iq['from'] = ifrom iq.enable('ping') return iq.send(timeout=timeout, callback=callback) async def ping(self, jid: Optional[JID] = None, ifrom: Optional[JID] = None, timeout: Optional[float] = None) -> float: """Send a ping request and calculate RTT. This is a coroutine. :param jid: The JID that will receive the ping. :raises IqError: When the remote entity answered an error :raises IqTimeout: When the remote entity did not answer """ own_host = False if not jid: if self.xmpp.is_component: jid = self.xmpp.server else: jid = self.xmpp.boundjid.host jid = JID(jid) if jid == self.xmpp.boundjid.host or \ self.xmpp.is_component and jid == self.xmpp.server: own_host = True if not timeout: timeout = self.timeout start = time.time() log.debug('Pinging %s', jid) try: await self.send_ping(jid, ifrom=ifrom, timeout=timeout) except IqError as e: if own_host: rtt = time.time() - start log.debug('Pinged %s, RTT: %s', jid, rtt) return rtt else: raise e else: rtt = time.time() - start log.debug('Pinged %s, RTT: %s', jid, rtt) return rtt slixmpp/slixmpp/plugins/xep_0199/stanza.py000066400000000000000000000013371477105560000210700ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import slixmpp from slixmpp.xmlstream import ElementBase class Ping(ElementBase): """ Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Example ping stanza: :: """ name = 'ping' namespace = 'urn:xmpp:ping' plugin_attrib = 'ping' interfaces = set() slixmpp/slixmpp/plugins/xep_0202/000077500000000000000000000000001477105560000171735ustar00rootroot00000000000000slixmpp/slixmpp/plugins/xep_0202/__init__.py000066400000000000000000000006201477105560000213020ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. from slixmpp.plugins.base import register_plugin from slixmpp.plugins.xep_0202 import stanza from slixmpp.plugins.xep_0202.stanza import EntityTime from slixmpp.plugins.xep_0202.time import XEP_0202 register_plugin(XEP_0202) slixmpp/slixmpp/plugins/xep_0202/stanza.py000066400000000000000000000065061477105560000210540ustar00rootroot00000000000000 # Slixmpp: The Slick XMPP Library # Copyright (C) 2010 Nathanael C. Fritz # This file is part of Slixmpp. # See the file LICENSE for copying permission. import datetime as dt from typing import Union from slixmpp.xmlstream import ElementBase from slixmpp.plugins import xep_0082 class EntityTime(ElementBase): """ The