Compare commits

..

74 Commits

Author SHA1 Message Date
mathieui
09d9320b91 Release 1.8.6 2024-12-26 22:39:49 +01:00
mathieui
fbf298c36d XEP-0030: fix tests
Use None as the "no value" value, rather than ''.
2024-12-26 09:42:33 +01:00
mathieui
7153d79006 itests/examples: integrate imghdr code directly (fix #3570)
Python 3.13 removes it, and I would rather not add another dependency
only for tests or examples…
2024-12-14 16:08:49 +01:00
mathieui
1d3e03a923 XEP-0030: make read operations more resilient
Reading valid XML which does not respect the XEP schema should not crash
the parser badly.
2024-12-14 16:00:24 +01:00
mathieui
3d0b09e2e2 xmlstream: prevent stanza parsing from crashing the whole thing
If an error happened while parsing a stanza, it would bring the whole
program down and disconnect instead of logging an error. And the DEBUG
log happened afterwards (so, never).
2024-12-14 15:52:51 +01:00
nicoco
23544731ef Support XEP-0490 (Message Display Synchronization) 2024-12-11 21:29:37 +00:00
nicoco
a18a6c4eb8 xep-0425: update to version 0.3.0 2024-12-11 21:24:12 +00:00
nicoco
dd903b1792 xep-0424: update to version 0.4.0 2024-12-11 21:24:12 +00:00
mathieui
cf3b30120e repo: remove github pull request template
codeberg annoyingly picks it up, which creates confusion for
contributors
2024-12-11 22:18:38 +01:00
jinyu
d86dccaf85 enhancement: Update type hints 2024-12-11 21:17:23 +00:00
jinyu
075812adf3 fix: Update msg_kwargs documentation to clarify required parameters for make_message 2024-12-11 21:16:37 +00:00
jinyu
8955ece461 Fix missing parameters 2024-12-11 21:16:37 +00:00
jinyu
5051c60262 enhancement: Refactor reply handling in XEP-0461; add fallback support and improve message construction 2024-12-11 21:16:37 +00:00
mathieui
c495eb73fc xmlstream: add stanza_not_sent event (fixes #3559) 2024-12-11 22:01:34 +01:00
jinyu
12c516d365 fix: Add future annotations import to base.py for improved type hinting 2024-11-19 18:44:33 +01:00
jinyu
d9b0b6dfe6 fix: Correct JID comparison in MUC plugin for bare JID matching 2024-11-19 18:44:27 +01:00
jinyu
7979e3b603 Resolve circular import 2024-11-19 18:44:20 +01:00
jinyu
f24a7679e5 enhancement: Improve JID handling and logging in MUC plugin; update status code return type 2024-11-19 18:44:11 +01:00
jinyu
df0ecfc142 Update the type of BasePlugin.xmpp to BaseXMPP. Increase the timeout for join_muc_wait. 2024-11-19 18:43:58 +01:00
jinyu
e79b98b266 add default timeout value 2024-11-19 18:43:49 +01:00
jinyu
5ed5e60b20 Fix: AttributeError in get_nick and jid_in_room due to incorrect JID type 2024-11-19 18:43:41 +01:00
mathieui
e5fe53ef45 tests: remove distutils use 2024-10-08 10:31:15 +02:00
mathieui
93608bd2f4 setup.py: Add python versions 2024-10-08 10:29:28 +02:00
nicoco
3b2386ee2f stanzabase: Pass problematic object to TypeError raised by ElementBase.append()
This should help debugging those type of errors.
2024-09-27 13:00:28 +02:00
nicoco
b94c6716f7 xep-0231: fix TypeError when BoB cannot be found
Although it is not documented whether the get_bob API
call can return None, the default implementation can,
which raises a TypeError when the requested BoB is not
available. This commit prevents such TypeError by
raising a XMPPError instead.

References: https://todo.sr.ht/~nicoco/slidcord/23
2024-09-27 10:28:24 +02:00
jinyu
db8ce9187c feat: Add is_correction method to XEP-0308 Last Message Correction plugin 2024-09-08 00:39:10 +08:00
mathieui
7f926a944a setup.py: Do not install aiodns on windows (fix #3556)
aiodns is broken on the default windows event loop and the workaround
has significant downsides. This has downsides too as it prevents
resolution of SRV records by default.
2024-08-25 13:18:31 +02:00
sch
e96f8e1ed0 Correct an MUC-related error message which indicates the requirement of a JID 2024-06-20 19:00:20 +00:00
sch
c8c0bb9134 Add BukuBot
Add BukuBot for bookmarks
2024-06-04 10:22:24 +00:00
schimoni
825c51b87d Add descriptive introduction and new links to project sendxmpp-py
Add descriptive introduction;
Change URL of repository sendxmpp-py;
Add groupchat to project sendxmpp-py.
2024-05-22 12:39:14 +00:00
mathieui
7c79f28587 XEP-0199: handle component case for keepalive ping 2024-03-22 20:48:36 +01:00
mathieui
dcaf812a28 ci: build cython module for itests 2024-02-09 23:28:15 +01:00
mathieui
ae4de043d2 itests: fix default server call 2024-02-09 23:11:29 +01:00
mathieui
998bbb80ad itests: hardcode default MUC server 2024-02-09 23:07:32 +01:00
mathieui
5a5b36ab39 xmlstream: make mypy even happier 2024-02-09 22:58:20 +01:00
mathieui
f151f0a7ab xmlstream/componentxmpp: fix some typing issues
Make mypy happier
2024-02-09 22:55:20 +01:00
mathieui
2424a3b36f slixtest: cleanup loop only if needed
if not, get_event_loop will throw, we can ignore this
2024-02-09 22:49:47 +01:00
mathieui
1c4bbbce8e ci: fix mypy step 2024-02-09 21:41:03 +01:00
mathieui
66d552d057 xep_0317: Fix compatibility with python < 3.9 2024-02-09 21:32:19 +01:00
nicoco
b8205a9ae4 Update plugin: XEP-0317 (hats)
Merge changes from nicoco's MR that I missed, improving tests and
interface.
2024-02-09 21:06:14 +01:00
nicoco
85b7210115 XEP-0264: Jingle Content Thumbnails (new plugin)
Cheogram actually uses it with SIMS to embed
a blurhash preview in the stanza.
2024-02-09 12:10:12 +01:00
nicoco
909c865524 XEP-0313: Do not try to parse date for fields without value.
Without this we end up passing "None"
instead of a str to the date parser,
which raises a TypeError.
It happens if you try to provide a form
to be filled, when slixmpp acts as a MAM
*server*.
2024-02-09 11:51:34 +01:00
nicoco
586d2f5107 XEP-0313: Add support for flipped page 2024-02-08 20:45:48 +01:00
nicoco
9f7260747f Add XEP_0461 to PluginDict 2024-02-08 20:34:16 +01:00
mathieui
c41209510a xep_049: implement bookmarks pinning stanzas 2024-02-04 11:59:36 +01:00
mathieui
9266486f46 xep_0317: add initial stanza support for hats 2024-02-04 11:32:24 +01:00
mathieui
5226858e0c Release 1.8.5 2024-02-02 01:59:31 +01:00
mathieui
7128ea249b Fix running process() with a timeout (closes #3505) 2024-02-02 01:00:25 +01:00
Maxime “pep” Buquet
992d80dd09 SCRAM: Restrict tls-unique to TLSv1.2
And prepare the code to work when CPython implements tls-exporter for
TLSv1.3.
This adds tls_version and binding_proposed attributes to the security
properties so we can detect if we were offerred channel binding SASL
mechanisms, and which TLS version we are on.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2024-02-02 00:56:40 +01:00
mathieui
c25305e80f componentxmpp: fix default host for components 2023-12-29 14:13:41 +01:00
mathieui
6765f84133 tests: close event loop at exit
prevents a nice segfault
2023-12-29 13:53:58 +01:00
mathieui
31fe7f7e06 [CI] add woodpecker CI 2023-12-29 13:53:58 +01:00
nicoco
84a7ac020f XEP-0461: rely on XEP-0428 for fallback
Breaks the previous fallback helpers, we now
rely on XEP-0461 instead
2023-12-28 16:38:37 +00:00
nicoco
331c1c1e21 XEP-0428: add fallback body and subject elements
+ tests
+ helpers to strip the fallback content
2023-12-28 16:38:37 +00:00
nicoco
28a60c22e2 ElementBase: add weak ref to parent when using append() 2023-12-28 16:38:37 +00:00
nicoco
af934b5bdf fix slixmpp.xmlstream.__all__ 2023-12-28 16:38:37 +00:00
genghis
897f876504 Correct Slixfeed title and add groupchat link to Stable Diffusion 2023-12-28 16:04:19 +00:00
genghis
2888be17ab Correct groupchat link for WhisperBot 2023-12-28 16:04:19 +00:00
genghis
975e31229c Correct links so they match to their respective text 2023-12-28 16:04:19 +00:00
genghis
6e9e66139d Add Stable Diffusion 2023-12-28 16:04:19 +00:00
genghis
380ac04d52 Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
9e5b530607 Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
71de274fab Update docs/projects.rst 2023-12-28 16:04:19 +00:00
genghis
5a0b02378d Add document Projects
Bots and Services utilizing Slixmpp
2023-12-28 16:04:19 +00:00
sxavier
9fc82e9e6f xep_0221: Add documentation overview and example 2023-12-28 16:01:19 +00:00
nicoco
ca90d3908e xep-0115: perf: avoid simultaneous disco info queries for the same verstring 2023-12-28 15:56:44 +00:00
Daniel Roschka
7de5cbcf33 Fix connect parameters used for follow-up calls
XMLStream.connect() is supposed to persist the parameters
it gets called with to allow follow-up calls to call
XMLStream.connect() without any parameters to result in a connection
with the same properties as the original one. That's for example used by
XMLStream.reconnect() when establishing a new connection.

Unfortunately that was broken for some of the parameters and resulted
different TLS related settings on reconnections. This commit fixes that.
2023-12-27 11:45:04 +01:00
Nicolas Cedilnik
76a11d4899 xep0356: implement IQ privilege
Also included:

- correctly handle privileges from different
  servers
- check that privileges have been granted before
  attempting to send something and raise
  PermissionError if not
- use dataclass and enums to store permissions instead of
  untyped dict
2023-12-19 14:14:16 +00:00
mathieui
dcfa0f20f9 [docs] add readthedocs.yaml 2023-11-13 19:38:48 +01:00
mathieui
7732af8991 Move references from lab.louiz.org to codeberg 2023-07-06 15:26:57 +02:00
nicoco
25c28ff5d1 xep_0461/add_quoted_fallback: add optional nickname argument
+ a little docstring that doesn't hurt
2023-06-05 20:48:38 +02:00
nicoco
e3e0d8f43e xep_0313/fin: add 'stable' and 'complete' attribs 2023-06-05 14:18:07 +02:00
nicoco
13729e47a6 add xeps 0385 and 0447 to plugins.PLUGINS 2023-06-05 14:18:07 +02:00
nicoco
f12860bfad fix misleading error msg
plugins.__all__ became plugins.PLUGINS a few commits ago
2023-06-05 14:18:07 +02:00
80 changed files with 2080 additions and 345 deletions

View File

@@ -1,13 +0,0 @@
################ Please use Gitlab instead of Github ###################################
Hello, thank you for contributing to slixmpp!
Youre about to open a pull request on github. However this github repository is not the official place for contributions on slixmpp.
Please open your merge request on https://lab.louiz.org/poezio/slixmpp/
You should be able to log in there with your github credentials, clone the slixmpp repository in your namespace, push your existing pull request into a new branch, and then open a merge request with one click, within 3 minutes.
This will help us review your contribution, avoid spreading things everywhere and it will even run the tests automatically with your changes.
Thank you.

22
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,22 @@
# .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

6
.woodpecker/lint.yml Normal file
View File

@@ -0,0 +1,6 @@
steps:
mypy:
image: python:3
commands:
- pip3 install mypy types-setuptools
- mypy slixmpp

View File

@@ -0,0 +1,10 @@
steps:
test_integration:
image: "python:3.11"
secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server]
commands:
- apt-get update
- apt-get install -y python3-pip cython3 gpg idn libidn-dev
- pip3 install emoji aiohttp aiodns
- python3 setup.py build_ext --inplace
- ./run_integration_tests.py

17
.woodpecker/test.yml Normal file
View File

@@ -0,0 +1,17 @@
steps:
unit_tests:
image: "python:${TAG}"
commands:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
matrix:
TAG:
- "3.7"
- "3.9"
- "3.8"
- "3.10"
- "3.11"
- "3.12"

View File

@@ -5,7 +5,7 @@ To contribute, the preferred way is to commit your changes on some
publicly-available git repository (on a fork `on github
<https://github.com/poezio/slixmpp>`_ or on your own repository) and to
notify the developers with either:
- a ticket `on the bug tracker <https://lab.louiz.org/poezio/slixmpp/issues/new>`_
- a ticket `on the bug tracker <https://codeberg.org/poezio/slixmpp/issues/new>`_
- a pull request on github
- a simple message on `the XMPP MUC <xmpp:slixmpp@muc.poez.io>`_

View File

@@ -8,13 +8,13 @@
<shortdesc xml:lang="en">Elegant Python library for XMPP</shortdesc>
<shortdesc xml:lang="fr">Bibliothèque pour XMPP élégante, en Python</shortdesc>
<homepage rdf:resource="https://lab.louiz.org/poezio/slixmpp/"/>
<download-page rdf:resource="https://lab.louiz.org/poezio/slixmpp/tags"/>
<bug-database rdf:resource="https://lab.louiz.org/poezio/slixmpp/issues"/>
<homepage rdf:resource="https://codeberg.org/poezio/slixmpp/"/>
<download-page rdf:resource="https://codeberg.org/poezio/slixmpp/tags"/>
<bug-database rdf:resource="https://codeberg.org/poezio/slixmpp/issues"/>
<developer-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/>
<support-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/>
<license rdf:resource="https://lab.louiz.org/poezio/slixmpp/blob/master/LICENSE"/>
<license rdf:resource="https://codeberg.org/poezio/slixmpp/raw/brach/master/LICENSE"/>
<language>en</language>
@@ -59,8 +59,8 @@
<repository>
<GitRepository>
<browse rdf:resource="https://lab.louiz.org/poezio/slixmpp"/>
<location rdf:resource="https://lab.louiz.org/poezio/slixmpp.git"/>
<browse rdf:resource="https://codeberg.org/poezio/slixmpp"/>
<location rdf:resource="https://codeberg.org/poezio/slixmpp.git"/>
</GitRepository>
</repository>
@@ -856,7 +856,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:version>0.4.0</xmpp:version>
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
@@ -864,7 +864,7 @@
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
<xmpp:version>0.3.0</xmpp:version>
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
@@ -909,6 +909,14 @@
<xmpp:note>no thumbnail support</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0490.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<release>
<Version>
@@ -1012,56 +1020,70 @@
<Version>
<revision>1.6.0</revision>
<created>2020-12-12</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.6.0/slixmpp-slix-1.6.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.6.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.7.0</revision>
<created>2021-01-29</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.0/slixmpp-slix-1.7.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.7.1</revision>
<created>2021-04-30</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.1/slixmpp-slix-1.7.1.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.1.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.0</revision>
<created>2022-02-27</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.0/slixmpp-slix-1.8.0.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.0.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.1</revision>
<created>2022-03-20</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.1/slixmpp-slix-1.8.1.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.1.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.2</revision>
<created>2022-04-06</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.2/slixmpp-slix-1.8.2.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.2.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.3</revision>
<created>2022-11-12</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.3/slixmpp-slix-1.8.3.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.3.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.4</revision>
<created>2023-05-28</created>
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.4/slixmpp-slix-1.8.4.tar.gz"/>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.4.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.5</revision>
<created>2024-02-02</created>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/>
</Version>
</release>
<release>
<Version>
<revision>1.8.6</revision>
<created>2024-12-26</created>
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.6.tar.gz"/>
</Version>
</release>
</Project>

View File

@@ -167,8 +167,9 @@ processing the same stanza twice.
- **Data:** :py:class:`~.Message`
- **Source:** :py:class:`BaseXMPP <.BaseXMPP>`
Makes the contents of message stanzas available whenever one is received. Be
sure to check the message type in order to handle error messages.
Makes the contents of message stanzas that include <body> tags available
whenever one is received.
Be sure to check the message type to handle error messages appropriately.
message_error
- **Data:** :py:class:`~.Message`

View File

@@ -11,7 +11,7 @@ Create and Run a Server Component
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
with `Git <https://lab.louiz.org/poezio/slixmpp>`_.
with `Git <https://codeberg.org/poezio/slixmpp>`_.
Many XMPP applications eventually graduate to requiring to run as a server
component in order to meet scalability requirements. To demonstrate how to

View File

@@ -11,7 +11,7 @@ Slixmpp Quickstart - Echo Bot
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
with `Git <https://lab.louiz.org/poezio/slixmpp>`_.
with `Git <https://codeberg.org/poezio/slixmpp>`_.
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
@@ -325,7 +325,7 @@ 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 <https://lab.louiz.org/poezio/slixmpp/tree/master/examples>`_.
can also be found in the Slixmpp `examples directory <https://codeberg.org/poezio/slixmpp/src/branch/master/examples>`_.
.. compound::

View File

@@ -11,7 +11,7 @@ Multi-User Chat (MUC) Bot
<xmpp:slixmpp@muc.poez.io?join>`_.
If you have not yet installed Slixmpp, do so now by either checking out a version
from `Git <https://lab.louiz.org/poezio/slixmpp>`_.
from `Git <https://codeberg.org/poezio/slixmpp>`_.
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

View File

@@ -4,9 +4,9 @@ Slixmpp
.. sidebar:: Get the Code
The latest source code for Slixmpp may be found on the `Git repo
<https://lab.louiz.org/poezio/slixmpp>`_. ::
<https://codeberg.org/poezio/slixmpp>`_. ::
git clone https://lab.louiz.org/poezio/slixmpp
git clone https://codeberg.org/poezio/slixmpp
An XMPP chat room is available for discussing and getting help with slixmpp.
@@ -14,7 +14,7 @@ Slixmpp
`slixmpp@muc.poez.io <xmpp:slixmpp@muc.poez.io?join>`_
**Reporting bugs**
You can report bugs at http://lab.louiz.org/poezio/slixmpp/issues.
You can report bugs at http://codeberg.org/poezio/slixmpp/issues.
Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.7+,

104
docs/projects.rst Normal file
View File

@@ -0,0 +1,104 @@
Projects 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 <https://code.moparisthebest.com/moparisthebest/sendxmpp-py>`_
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`_
Bots
----
BotLogMauve
~~~~~~~~~~~
XMPP bot which logs groupchat messages. Logs are in text format, with one file per day and per groupchat.
- `Source <https://git.khaganat.net/khaganat/BotLogMauve>`_
BukuBot
~~~~~~~
BukuBot makes it possible to manage and search your bookmarks from your chat.
- `Source <https://codeberg.org/sch/BukuBot>`_
LinkBot
~~~~~~~
This bot reveals the title of any shared link in a groupchat for quick content insight.
- `Source <https://git.xmpp-it.net/mario/XMPPBot>`_
llama-bot
~~~~~~~~~
Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://github.com/decent-im/llama-bot>`_
- `Demo <xmpp:llama@decent.im?message>`_
Morbot
~~~~~~
Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/Morbot>`_
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 <xmpp:slixfeed@chat.woodpeckersnest.space?join>`_
- `Source <https://gitgud.io/sjehuda/slixfeed>`_
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.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Homepage <https://sms4you-team.pages.debian.net/sms4you/>`_
- `Source <https://salsa.debian.org/sms4you-team/sms4you>`_
Stable Diffusion
~~~~~~~~~~~~~~~~
XMPP bot that generates digital images from textual descriptions.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Source <https://www.nicoco.fr/blog/2022/08/31/xmpp-bot-stable-diffusion/>`_
WhisperBot
~~~~~~~~~~
XMPP bot that transliterates audio messages using OpenAI's Whisper libraries.
- `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/WhisperBot>`_
XMPP MUC Message Gateway
~~~~~~~~~~~~~~~~~~~~~~~~
A multipurpose JSON forwarder microservice from HTTP POST to XMPP MUC room over TLSv1.2 with SliXMPP.
- `Source <https://github.com/immanuelfodor/xmpp-muc-message-gateway>`_
Services
--------
AtomToPubsub
~~~~~~~~~~~~
AtomToPubsub is a simple Python script that parses Atom + RSS feeds and pushes the entries to a designated XMPP Pubsub Node.
- `Groupchat <xmpp:movim@conference.movim.eu?join>`_
- `Source <https://github.com/imattau/atomtopubsub>`_
Slidge
~~~~~~
Slidge is a general purpose XMPP gateway framework in Python.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Homepage <https://slidge.im/core/>`_
- `Source <https://sr.ht/~nicoco/slidge>`_

184
examples/imghdr.py Normal file
View File

@@ -0,0 +1,184 @@
"""
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()

184
itests/imghdr.py Normal file
View File

@@ -0,0 +1,184 @@
"""
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()

View File

@@ -10,7 +10,7 @@ UNIQUE = uuid4().hex
class TestMUC(SlixIntegration):
async def asyncSetUp(self):
self.mucserver = self.envjid('CI_MUC_SERVER')
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'),

View File

@@ -5,7 +5,7 @@ import logging
import unittest
from argparse import ArgumentParser
from distutils.core import Command
from setuptools import Command
from importlib import import_module
from pathlib import Path

View File

@@ -5,7 +5,7 @@ import logging
import unittest
from argparse import ArgumentParser
from distutils.core import Command
from setuptools import Command
from importlib import import_module
from pathlib import Path

View File

@@ -33,12 +33,17 @@ CLASSIFIERS = [
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'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',
]
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
def check_include(library_name, header):
command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name]
try:
@@ -59,6 +64,7 @@ def check_include(library_name, header):
print('%s headers not found.' % library_name)
return False
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
@@ -80,14 +86,14 @@ setup(
long_description=LONG_DESCRIPTION,
author='Florent Le Coz',
author_email='louiz@louiz.org',
url='https://lab.louiz.org/poezio/slixmpp',
url='https://codeberg.org/poezio/slixmpp',
license='MIT',
platforms=['any'],
package_data={'slixmpp': ['py.typed']},
packages=packages,
ext_modules=ext_modules,
install_requires=[
'aiodns>=1.0',
'aiodns >= 1.0; sys_platform=="linux" or sys_platform=="darwin"',
'pyasn1',
'pyasn1_modules',
'typing_extensions; python_version < "3.8.0"',

View File

@@ -27,3 +27,9 @@ 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__'
]

View File

@@ -285,7 +285,7 @@ class BaseXMPP(XMLStream):
if plugin in plugins.PLUGINS:
self.register_plugin(plugin)
else:
raise NameError("Plugin %s not in plugins.__all__." % plugin)
raise NameError("Plugin %s not in plugins.PLUGINS." % plugin)
def __getitem__(self, key):
"""Return a plugin given its name, if it has been registered."""

View File

@@ -138,8 +138,8 @@ class ClientXMPP(BaseXMPP):
self.credentials['password'] = value
def connect(self, address: Optional[Tuple[str, int]] = None, # type: ignore
use_ssl: bool = False, force_starttls: bool = True,
disable_starttls: bool = False) -> None:
use_ssl: Optional[bool] = None, force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Connect to the XMPP server.
When no address is given, a SRV lookup for the server will
@@ -166,8 +166,8 @@ class ClientXMPP(BaseXMPP):
host, port = (self.boundjid.host, 5222)
self.dns_service = 'xmpp-client'
return XMLStream.connect(self, host, port, use_ssl=use_ssl,
force_starttls=force_starttls, disable_starttls=disable_starttls)
XMLStream.connect(self, host, port, use_ssl=use_ssl,
force_starttls=force_starttls, disable_starttls=disable_starttls)
def register_feature(self, name: str, handler: Callable, restart: bool = False, order: int = 5000) -> None:
"""Register a stream feature handler.

View File

@@ -9,6 +9,8 @@
import logging
import hashlib
from typing import Optional
from slixmpp import Message, Iq, Presence
from slixmpp.basexmpp import BaseXMPP
from slixmpp.stanza import Handshake
@@ -93,7 +95,9 @@ class ComponentXMPP(BaseXMPP):
for st in Message, Iq, Presence:
register_stanza_plugin(st, Error)
def connect(self, host=None, port=None, use_ssl=False):
def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Connect to the server.
@@ -103,17 +107,18 @@ class ComponentXMPP(BaseXMPP):
Defauts to :attr:`server_port`.
:param use_ssl: Flag indicating if SSL should be used by connecting
directly to a port using SSL.
:param force_starttls: UNUSED
:param disable_starttls: UNUSED
"""
if host is None:
host = self.server_host
if port is None:
port = self.server_port
if host is not None:
self.server_host = host
if port:
self.server_port = port
self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=host, port=port,
use_ssl=use_ssl)
XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl)
def incoming_filter(self, xml):
"""

View File

@@ -37,7 +37,8 @@ class FeatureMechanisms(BasePlugin):
'unencrypted_digest': False,
'unencrypted_cram': False,
'unencrypted_scram': True,
'order': 100
'order': 100,
'tls_version': None,
}
def plugin_init(self):
@@ -96,7 +97,20 @@ class FeatureMechanisms(BasePlugin):
result[value] = creds.get('email', jid)
elif value == 'channel_binding':
if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)):
result[value] = self.xmpp.socket.get_channel_binding()
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':
@@ -121,6 +135,11 @@ class FeatureMechanisms(BasePlugin):
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

View File

@@ -76,6 +76,7 @@ PLUGINS = [
'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. Dont automatically load
'xep_0279', # Server IP Check
'xep_0280', # Message Carbons
@@ -85,6 +86,7 @@ PLUGINS = [
# 'xep_0302', # XMPP Compliance Suites 2012. Dont 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. Dont automatically load
# 'xep_0325', # IoT Systems Control. Dont automatically load
@@ -101,6 +103,7 @@ PLUGINS = [
'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
@@ -109,13 +112,16 @@ PLUGINS = [
'xep_0421', # Anonymous unique occupant identifiers for MUCs
'xep_0422', # Message Fastening
'xep_0424', # Message Retraction
'xep_0425', # Message Moderation
'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_0447', # Stateless file sharing
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
'xep_0490', # Message Displayed Synchronization
# Meant to be imported by plugins
]

View File

@@ -6,14 +6,18 @@
# 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
from typing import Any, Dict, Set, ClassVar, Union, TYPE_CHECKING
if TYPE_CHECKING:
from slixmpp.clientxmpp import ClientXMPP
from slixmpp.componentxmpp import ComponentXMPP
log = logging.getLogger(__name__)
@@ -272,7 +276,7 @@ class BasePlugin(object):
#: `plugin.config['foo']`.
default_config: ClassVar[Dict[str, Any]] = {}
def __init__(self, xmpp, config=None):
def __init__(self, xmpp: Union[ClientXMPP,ComponentXMPP], config=None):
self.xmpp = xmpp
if self.xmpp:
self.api = self.xmpp.api.wrap(self.name)

View File

@@ -165,11 +165,11 @@ class DiscoInfo(ElementBase):
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 = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None))
id = (category, type_, xml_lang, name)
if isinstance(identities, set):
identities.add(id)
else:
@@ -253,10 +253,12 @@ class DiscoInfo(ElementBase):
else:
features = []
for feature_xml in self.xml.findall('{%s}feature' % self.namespace):
if isinstance(features, set):
features.add(feature_xml.attrib['var'])
else:
features.append(feature_xml.attrib['var'])
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]):

View File

@@ -49,11 +49,13 @@ from slixmpp.plugins.xep_0045.stanza import (
MUCUserItem,
)
from slixmpp.types import (
JidStr,
MucRole,
MucAffiliation,
MucRoomItem,
MucRoomItemKeys,
PresenceArgs,
PresenceShows,
)
JoinResult = Tuple[Presence, Message, List[Presence], List[Message]]
@@ -187,7 +189,7 @@ class XEP_0045(BasePlugin):
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)
self.xmpp.event('muc::%s::config_status' % msg['from'].bare, msg)
def _client_handle_presence(self, pr: Presence):
"""As a client, handle a presence stanza"""
@@ -264,7 +266,7 @@ class XEP_0045(BasePlugin):
seconds: Optional[int] = None,
since: Optional[datetime] = None,
presence_options: Optional[PresenceArgs] = None,
timeout: Optional[int] = None) -> JoinResult:
timeout: int = 300) -> JoinResult:
"""
Try to join a MUC and block until we are joined or get an error.
@@ -310,7 +312,7 @@ class XEP_0045(BasePlugin):
stanza.send()
return await self._await_join(room, timeout)
async def _await_join(self, room: JID, timeout: Optional[int] = None) -> JoinResult:
async def _await_join(self, room: JID, timeout: int = 300) -> JoinResult:
"""Do the heavy lifting for awaiting a MUC join
A muc join, once the join stanza is sent, is:
@@ -358,7 +360,7 @@ class XEP_0045(BasePlugin):
return (pres, subject, occupant_buffer, history_buffer)
def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
pstatus='', pshow='', pfrom='') -> asyncio.Future:
pstatus='', pshow: PresenceShows='chat', pfrom: JidStr='') -> asyncio.Future:
""" Join the specified room, requesting 'maxhistory' lines of history.
.. deprecated:: 1.8.0
@@ -412,7 +414,7 @@ class XEP_0045(BasePlugin):
)
del self.rooms[room]
def set_subject(self, room: JID, subject: str, *, mfrom: Optional[JID] = None):
def set_subject(self, room: JidStr, subject: str, *, mfrom: Optional[JID] = None):
"""Set a rooms subject.
:param room: JID of the room.
@@ -423,7 +425,7 @@ class XEP_0045(BasePlugin):
msg['subject'] = subject
msg.send()
async def get_room_config(self, room: JID, ifrom: Optional[JID] = None,
async def get_room_config(self, room: JidStr, ifrom: Optional[JID] = None,
**iqkwargs) -> Form:
"""Get the room config form in 0004 plugin format.
@@ -438,7 +440,7 @@ class XEP_0045(BasePlugin):
raise ValueError("Configuration form not found")
return form
async def set_room_config(self, room: JID, config: Form, *,
async def set_room_config(self, room: JidStr, config: Form, *,
ifrom: Optional[JID] = None, **iqkwargs):
"""Send a room config form.
@@ -451,8 +453,8 @@ class XEP_0045(BasePlugin):
iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
await iq.send(**iqkwargs)
async def cancel_config(self, room: JID, *,
ifrom: Optional[JID] = None, **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.
@@ -462,8 +464,8 @@ class XEP_0045(BasePlugin):
iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
await iq.send(**iqkwargs)
async def destroy(self, room: JID, reason: str = '', altroom: Optional[JID] = None, *,
ifrom: Optional[JID] = None, **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.
@@ -479,10 +481,10 @@ class XEP_0045(BasePlugin):
iq['mucowner_query']['destroy']['reason'] = reason
await iq.send(**iqkwargs)
async def set_affiliation(self, room: JID, affiliation: MucAffiliation, *,
jid: Optional[JID] = None,
async def set_affiliation(self, room: JidStr, affiliation: MucAffiliation, *,
jid: Optional[JidStr] = None,
nick: Optional[str] = None, reason: str = '',
ifrom: Optional[JID] = None, **iqkwargs):
ifrom: Optional[JidStr] = None, **iqkwargs):
""" Change room affiliation for a JID or nickname.
:param room: Room to modify.
@@ -493,7 +495,7 @@ class XEP_0045(BasePlugin):
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 a using a 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)
@@ -506,8 +508,8 @@ class XEP_0045(BasePlugin):
iq['mucadmin_query']['item']['reason'] = reason
await iq.send(**iqkwargs)
async def get_affiliation_list(self, room: JID, affiliation: MucAffiliation, *,
ifrom: Optional[JID] = None, **iqkwargs) -> List[JID]:
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.
@@ -518,9 +520,9 @@ class XEP_0045(BasePlugin):
result = await iq.send(**iqkwargs)
return [item['jid'] for item in result['mucadmin_query']]
async def send_affiliation_list(self, room: JID,
affiliations: List[Tuple[JID, MucAffiliation]], *,
ifrom: Optional[JID] = None, **iqkwargs):
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.
@@ -534,8 +536,8 @@ class XEP_0045(BasePlugin):
iq['mucadmin_query'].append(item)
await iq.send(**iqkwargs)
async def set_role(self, room: JID, nick: str, role: MucRole, *,
reason: str = '', ifrom: Optional[JID] = None, **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
@@ -555,8 +557,8 @@ class XEP_0045(BasePlugin):
iq['mucadmin_query']['item']['reason'] = reason
await iq.send(**iqkwargs)
async def get_roles_list(self, room: JID, role: MucRole, *,
ifrom: Optional[JID] = None, **iqkwargs) -> List[str]:
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.
@@ -567,8 +569,8 @@ class XEP_0045(BasePlugin):
result = await iq.send(**iqkwargs)
return [item['nick'] for item in result['mucadmin_query']]
async def send_role_list(self, room: JID, roles: List[Tuple[str, MucRole]], *,
ifrom: Optional[JID] = None, **iqkwargs):
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.
@@ -582,8 +584,8 @@ class XEP_0045(BasePlugin):
iq['mucadmin_query'].append(item)
await iq.send(**iqkwargs)
def invite(self, room: JID, jid: JID, reason: str = '', *,
mfrom: Optional[JID] = None):
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.
@@ -596,8 +598,8 @@ class XEP_0045(BasePlugin):
msg['muc']['invite']['reason'] = reason
self.xmpp.send(msg)
def invite_server(self, room: JID, jid: JID,
invite_from: JID, reason: str = ''):
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
@@ -615,8 +617,8 @@ class XEP_0045(BasePlugin):
msg['muc']['invite']['reason'] = reason
msg.send()
def decline(self, room: JID, jid: JID, reason: str = '', *,
mfrom: Optional[JID] = None):
def decline(self, room: JidStr, jid: JidStr, reason: str = '', *,
mfrom: Optional[JidStr] = None):
"""Decline a mediated invitation.
:param room: Room the invitation came from.
@@ -629,7 +631,7 @@ class XEP_0045(BasePlugin):
msg['muc']['decline']['reason'] = reason
self.xmpp.send(msg)
def request_voice(self, room: JID, role: str, *, mfrom: Optional[JID] = None):
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.
@@ -646,29 +648,49 @@ class XEP_0045(BasePlugin):
"""Check if a JID is present in a room.
:param room: Room to check.
:param jid: JID to check.
:param jid: FULL JID to check.
"""
bare_match = False
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if not entry.get('jid'):
continue
if entry is not None and entry['jid'].full == jid:
if entry['jid'] == jid.full:
return True
return False
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) -> Optional[str]:
"""Get the nickname of a specific JID in a room.
:param room: Room to inspect.
:param jid: JID whose nick to return.
:param jid: FULL JID whose nick to return.
"""
bare_match = None
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if not entry.get('jid'):
continue
if entry is not None and entry['jid'].full == jid:
if entry['jid'] == jid.full:
return nick
return None
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) -> List[JID]:
"""Get the list of rooms we sent a join presence to
@@ -704,7 +726,7 @@ class XEP_0045(BasePlugin):
raise ValueError("Room %s is not joined" % room)
return list(self.rooms[room].keys())
def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
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)

View File

@@ -28,7 +28,7 @@ class MUCBase(ElementBase):
plugin_attrib = 'muc'
interfaces = {'affiliation', 'role', 'jid', 'nick', 'room', 'status_codes'}
def get_status_codes(self) -> Set[str]:
def get_status_codes(self) -> Set[int]:
status = self.xml.findall(f'{{{NS_USER}}}status')
return {int(status.attrib['code']) for status in status}
@@ -275,7 +275,8 @@ class MUCUserItem(ElementBase):
jid = self.xml.attrib.get('jid', None)
if jid:
return JID(jid)
return jid
else:
return None
class MUCActor(ElementBase):
@@ -288,7 +289,8 @@ class MUCActor(ElementBase):
jid = self.xml.attrib.get('jid', None)
if jid:
return JID(jid)
return jid
else:
return None
class MUCDestroy(ElementBase):

View File

@@ -7,7 +7,8 @@ import logging
import hashlib
import base64
from asyncio import Future
from asyncio import Future, Lock
from collections import defaultdict
from typing import Optional
from slixmpp import __version__
@@ -94,6 +95,9 @@ class XEP_0115(BasePlugin):
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)
@@ -137,7 +141,7 @@ class XEP_0115(BasePlugin):
self.xmpp.event('entity_caps', p)
async def _process_caps(self, pres):
async def _process_caps(self, pres: Presence):
if not pres['caps']['hash']:
log.debug("Received unsupported legacy caps: %s, %s, %s",
pres['caps']['node'],
@@ -147,7 +151,11 @@ class XEP_0115(BasePlugin):
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

View File

@@ -137,7 +137,14 @@ class XEP_0199(BasePlugin):
async def _keepalive(self, event=None):
log.debug("Keepalive ping...")
try:
rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
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.")

View File

@@ -15,6 +15,32 @@ log = logging.getLogger(__name__)
class XEP_0221(BasePlugin):
"""
XEP-0221: Data Forms Media Element
In certain implementations of Data Forms (XEP-0004), it can be
helpful to include media data such as small images. One example is
CAPTCHA Forms (XEP-0158). This plugin implements a method for
including media data in a data form.
Typical use pattern:
.. code-block:: python
self.register_plugin('xep_0221')
self['xep_0050'].add_command(node="showimage",
name="Show my image",
handler=self.form_handler)
def form_handler(self,iq,session):
image_url="https://xmpp.org/images/logos/xmpp-logo.svg"
form=self['xep_0004'].make_form('result','My Image')
form.addField(var='myimage', ftype='text-single', label='My Image', value=image_url)
form.field['myimage']['media'].add_uri(value=image_url, itype="image/svg")
session['payload']=form
return session
"""
name = 'xep_0221'
description = 'XEP-0221: Data Forms Media Element'

View File

@@ -20,6 +20,18 @@ class XEP_0223(BasePlugin):
"""
XEP-0223: Persistent Storage of Private Data via PubSub
If a specific pubsub node requires additional publish options, edit the
:attr:`.node_profile` attribute of this plugin:
.. code-block:: python
self.xmpp.plugin["xep_0223"].node_profiles["urn:some:node"] = {
"pubsub#max_items" = "max"
}
This makes :meth:`.store` add these publish options whenever it is called
for the ``urn:some:node`` node.
"""
name = 'xep_0223'
@@ -28,6 +40,7 @@ class XEP_0223(BasePlugin):
profile = {'pubsub#persist_items': True,
'pubsub#access_model': 'whitelist'}
node_profiles = dict[str, dict[str, str]]()
def configure(self, node: str, **iqkwargs) -> Future:
"""
@@ -70,7 +83,8 @@ class XEP_0223(BasePlugin):
value='http://jabber.org/protocol/pubsub#publish-options')
fields = options['fields']
for field, value in self.profile.items():
profile = self.profile | self.node_profiles.get(node, {})
for field, value in profile.items():
if field not in fields:
options.add_field(var=field)
options.get_fields()[field]['value'] = value

View File

@@ -10,6 +10,7 @@ from asyncio import Future
from typing import Optional
from slixmpp import JID
from slixmpp.exceptions import XMPPError
from slixmpp.stanza import Iq, Message, Presence
from slixmpp.xmlstream.handler import CoroutineCallback
from slixmpp.xmlstream.matcher import StanzaPath
@@ -139,6 +140,13 @@ class XEP_0231(BasePlugin):
self.xmpp.event('bob', iq)
elif iq['type'] == 'get':
data = await self.api['get_bob'](iq['to'], None, iq['from'], args=cid)
if data is None:
raise XMPPError(
"item-not-found",
f"Bits of binary '{cid}' is not available",
)
if isinstance(data, Iq):
data['id'] = iq['id']
data.send()

View File

@@ -0,0 +1,5 @@
from slixmpp.plugins.base import register_plugin
from .thumbnail import XEP_0264
register_plugin(XEP_0264)

View File

@@ -0,0 +1,36 @@
from typing import Optional
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0234.stanza import File
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:thumbs:1"
class Thumbnail(ElementBase):
name = plugin_attrib = "thumbnail"
namespace = NS
interfaces = {"uri", "media-type", "width", "height"}
def get_width(self) -> int:
return _int_or_none(self._get_attr("width"))
def get_height(self) -> int:
return _int_or_none(self._get_attr("height"))
def set_width(self, v: int) -> None:
self._set_attr("width", str(v))
def set_height(self, v: int) -> None:
self._set_attr("height", str(v))
def _int_or_none(v) -> Optional[int]:
try:
return int(v)
except ValueError:
return None
def register_plugin():
register_stanza_plugin(File, Thumbnail)

View File

@@ -0,0 +1,24 @@
import logging
from slixmpp.plugins import BasePlugin
from . import stanza
log = logging.getLogger(__name__)
class XEP_0264(BasePlugin):
"""
XEP-0264: Jingle Content Thumbnails
Can also be used with 0385 (Stateless inline media sharing)
"""
name = "xep_0264"
description = "XEP-0264: Jingle Content Thumbnails"
dependencies = {"xep_0234"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -5,7 +5,6 @@
# 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
@@ -45,5 +44,8 @@ class XEP_0308(BasePlugin):
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
def _handle_correction(self, msg):
def is_correction(self, msg: Message):
return msg.xml.find('{%s}replace' % Replace.namespace) is not None
def _handle_correction(self, msg: Message):
self.xmpp.event('message_correction', msg)

View File

@@ -52,9 +52,10 @@ class MAM(ElementBase):
#: fetch, not relevant for the stanza itself.
interfaces = {
'queryid', 'start', 'end', 'with', 'results',
'before_id', 'after_id', 'ids',
'before_id', 'after_id', 'ids', 'flip_page',
}
sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids'}
sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids',
'flip_page'}
def setup(self, xml=None):
ElementBase.setup(self, xml)
@@ -81,7 +82,7 @@ class MAM(ElementBase):
def get_start(self) -> Optional[datetime]:
fields = self.get_fields()
field = fields.get('start')
if field:
if field and field["value"]:
return xep_0082.parse(field['value'])
return None
@@ -94,7 +95,7 @@ class MAM(ElementBase):
def get_end(self) -> Optional[datetime]:
fields = self.get_fields()
field = fields.get('end')
if field:
if field and field["value"]:
return xep_0082.parse(field['value'])
return None
@@ -168,6 +169,8 @@ class MAM(ElementBase):
def del_results(self):
self._results = []
def get_flip_page(self):
return self.xml.find(f'{{{self.namespace}}}flip-page') is not None
class Fin(ElementBase):
"""A MAM fin element (end of query).
@@ -187,7 +190,7 @@ class Fin(ElementBase):
name = 'fin'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_fin'
interfaces = {'results'}
interfaces = {'results', 'stable', 'complete'}
def setup(self, xml=None):
ElementBase.setup(self, xml)

View File

@@ -0,0 +1,11 @@
# Slixmpp: The Slick XMPP Library
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins import register_plugin
from slixmpp.plugins.xep_0317 import stanza
from slixmpp.plugins.xep_0317.hats import XEP_0317
from slixmpp.plugins.xep_0317.stanza import Hat, Hats
register_plugin(XEP_0317)
__all__ = ['stanza', 'XEP_317']

View File

@@ -0,0 +1,16 @@
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0317(BasePlugin):
"""
XEP-0317: Hats
"""
name = 'xep_0317'
description = 'XEP-0317: Hats'
dependencies = {'xep_0030', 'xep_0045', 'xep_0050'}
stanza = stanza
namespace = stanza.NS
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,58 @@
from slixmpp import Presence
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
from typing import List, Tuple
NS = 'urn:xmpp:hats:0'
class Hats(ElementBase):
"""
Hats element, container for multiple hats:
.. code-block::xml
<hats xmlns='urn:xmpp:hats:0'>
<hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/>
</hat>
<hat title='Presenter' uri='http://schemas.example.com/hats#presenter' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#EC0524"/>
</hat>
</hats>
"""
name = 'hats'
namespace = NS
plugin_attrib = 'hats'
def add_hats(self, data: List[Tuple[str, str]]) -> None:
for uri, title in data:
hat = Hat()
hat["uri"] = uri
hat["title"] = title
self.append(hat)
class Hat(ElementBase):
"""
Hat element, has a title and url, may contain arbitrary sub-elements.
.. code-block::xml
<hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'>
<badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/>
</hat>
"""
name = 'hat'
plugin_attrib = 'hat'
namespace = NS
interfaces = {'title', 'uri'}
plugin_multi_attrib = "hats"
def register_plugin() -> None:
register_stanza_plugin(Hats, Hat, iterable=True)
register_stanza_plugin(Presence, Hats)

View File

@@ -1,7 +1,7 @@
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0356 import stanza
from slixmpp.plugins.xep_0356.stanza import Perm, Privilege
from slixmpp.plugins.xep_0356.privilege import XEP_0356
from . import stanza
from .privilege import XEP_0356
from .stanza import Perm, Privilege
register_plugin(XEP_0356)

View File

@@ -0,0 +1,36 @@
import dataclasses
from collections import defaultdict
from enum import Enum
class RosterAccess(str, Enum):
NONE = "none"
GET = "get"
SET = "set"
BOTH = "both"
class MessagePermission(str, Enum):
NONE = "none"
OUTGOING = "outgoing"
class IqPermission(str, Enum):
NONE = "none"
GET = "get"
SET = "set"
BOTH = "both"
class PresencePermission(str, Enum):
NONE = "none"
MANAGED_ENTITY = "managed_entity"
ROSTER = "roster"
@dataclasses.dataclass
class Permissions:
roster = RosterAccess.NONE
message = MessagePermission.NONE
iq = defaultdict(lambda: IqPermission.NONE)
presence = PresencePermission.NONE

View File

@@ -1,14 +1,16 @@
import logging
import typing
import uuid
from collections import defaultdict
from slixmpp import Message, JID, Iq
from slixmpp import JID, Iq, Message
from slixmpp.plugins.base import BasePlugin
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0356 import stanza, Privilege, Perm
from slixmpp.xmlstream.matcher import StanzaPath
from . import stanza
from .permissions import IqPermission, MessagePermission, Permissions, RosterAccess
log = logging.getLogger(__name__)
@@ -29,7 +31,7 @@ class XEP_0356(BasePlugin):
dependencies = {"xep_0297"}
stanza = stanza
granted_privileges = {"roster": "none", "message": "none", "presence": "none"}
granted_privileges = defaultdict(Permissions)
def plugin_init(self):
if not self.xmpp.is_component:
@@ -49,32 +51,42 @@ class XEP_0356(BasePlugin):
def plugin_end(self):
self.xmpp.remove_handler("Privileges")
def _handle_privilege(self, msg: Message):
def _handle_privilege(self, msg: StanzaBase):
"""
Called when the XMPP server advertise the component's privileges.
Stores the privileges in this instance's granted_privileges attribute (a dict)
and raises the privileges_advertised event
"""
permissions = self.granted_privileges[msg.get_from()]
for perm in msg["privilege"]["perms"]:
self.granted_privileges[perm["access"]] = perm["type"]
access = perm["access"]
if access == "iq":
for ns in perm["namespaces"]:
permissions.iq[ns["ns"]] = ns["type"]
elif access in _VALID_ACCESSES:
setattr(permissions, access, perm["type"])
else:
log.warning("Received an invalid privileged access: %s", access)
log.debug(f"Privileges: {self.granted_privileges}")
self.xmpp.event("privileges_advertised")
def send_privileged_message(self, msg: Message):
if self.granted_privileges["message"] == "outgoing":
self._make_privileged_message(msg).send()
else:
log.error(
if (
self.granted_privileges[msg.get_from().domain].message
!= MessagePermission.OUTGOING
):
raise PermissionError(
"The server hasn't authorized us to send messages on behalf of other users"
)
else:
self._make_privileged_message(msg).send()
def _make_privileged_message(self, msg: Message):
stanza = self.xmpp.make_message(
mto=self.xmpp.server_host, mfrom=self.xmpp.boundjid.bare
)
stanza["privilege"]["forwarded"].append(msg)
return stanza
server = msg.get_from().domain
wrapped = self.xmpp.make_message(mto=server, mfrom=self.xmpp.boundjid.bare)
wrapped["privilege"]["forwarded"].append(msg)
return wrapped
def _make_get_roster(self, jid: typing.Union[JID, str], **iq_kwargs):
return self.xmpp.make_iq_get(
@@ -106,9 +118,15 @@ class XEP_0356(BasePlugin):
:param jid: user we want to fetch the roster from
"""
if self.granted_privileges["roster"] not in ("get", "both"):
log.error("The server did not grant us privileges to get rosters")
raise ValueError
if isinstance(jid, str):
jid = JID(jid)
if self.granted_privileges[jid.domain].roster not in (
RosterAccess.GET,
RosterAccess.BOTH,
):
raise PermissionError(
"The server did not grant us privileges to get rosters"
)
else:
return await self._make_get_roster(jid).send(**send_kwargs)
@@ -137,8 +155,56 @@ class XEP_0356(BasePlugin):
},
}
"""
if self.granted_privileges["roster"] not in ("set", "both"):
log.error("The server did not grant us privileges to set rosters")
raise ValueError
if isinstance(jid, str):
jid = JID(jid)
if self.granted_privileges[jid.domain].roster not in (
RosterAccess.GET,
RosterAccess.BOTH,
):
raise PermissionError(
"The server did not grant us privileges to set rosters"
)
else:
return await self._make_set_roster(jid, roster_items).send(**send_kwargs)
async def send_privileged_iq(
self, encapsulated_iq: Iq, iq_id: typing.Optional[str] = None
):
"""
Send an IQ on behalf of a user
Caution: the IQ *must* have the jabber:client namespace
"""
iq_id = iq_id or str(uuid.uuid4())
encapsulated_iq["id"] = iq_id
server = encapsulated_iq.get_to().domain
perms = self.granted_privileges.get(server)
if not perms:
raise PermissionError(f"{server} has not granted us any privilege")
itype = encapsulated_iq["type"]
for ns in encapsulated_iq.plugins.values():
type_ = perms.iq[ns.namespace]
if type_ == IqPermission.NONE:
raise PermissionError(
f"{server} has not granted any IQ privilege for namespace {ns.namespace}"
)
elif type_ == IqPermission.BOTH:
pass
elif type_ != itype:
raise PermissionError(
f"{server} has not granted IQ {itype} privilege for namespace {ns.namespace}"
)
iq = self.xmpp.make_iq(
itype=itype,
ifrom=self.xmpp.boundjid.bare,
ito=encapsulated_iq.get_from(),
id=iq_id,
)
iq["privileged_iq"].append(encapsulated_iq)
resp = await iq.send()
return resp["privilege"]["forwarded"]["iq"]
# does not include iq access that is handled differently
_VALID_ACCESSES = {"message", "roster", "presence"}

View File

@@ -1,13 +1,12 @@
from slixmpp.stanza import Message
from slixmpp.xmlstream import (
ElementBase,
register_stanza_plugin,
)
from slixmpp.plugins.xep_0297 import Forwarded
from slixmpp.stanza import Iq, Message
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = "urn:xmpp:privilege:2"
class Privilege(ElementBase):
namespace = "urn:xmpp:privilege:2"
namespace = NS
name = "privilege"
plugin_attrib = "privilege"
@@ -25,26 +24,40 @@ class Privilege(ElementBase):
def presence(self):
return self.permission("presence")
def iq(self):
return self.permission("iq")
def add_perm(self, access, type):
def add_perm(self, access, type_):
# This should only be needed for servers, so maybe out of scope for slixmpp
perm = Perm()
perm["type"] = type
perm["type"] = type_
perm["access"] = access
self.append(perm)
class Perm(ElementBase):
namespace = "urn:xmpp:privilege:2"
namespace = NS
name = "perm"
plugin_attrib = "perm"
plugin_multi_attrib = "perms"
interfaces = {"type", "access"}
class NameSpace(ElementBase):
namespace = NS
name = "namespace"
plugin_attrib = "namespace"
plugin_multi_attrib = "namespaces"
interfaces = {"ns", "type"}
class PrivilegedIq(ElementBase):
namespace = NS
name = "privileged_iq"
plugin_attrib = "privileged_iq"
def register():
register_stanza_plugin(Message, Privilege)
register_stanza_plugin(Iq, Privilege)
register_stanza_plugin(Privilege, Forwarded)
register_stanza_plugin(Privilege, Perm, iterable=True)
register_stanza_plugin(Perm, NameSpace, iterable=True)
register_stanza_plugin(Iq, PrivilegedIq)

View File

@@ -4,7 +4,6 @@
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0424.stanza import *
from slixmpp.plugins.xep_0424.retraction import XEP_0424
from .retraction import XEP_0424
register_plugin(XEP_0424)

View File

@@ -31,7 +31,7 @@ class XEP_0424(BasePlugin):
stanza.register_plugins()
self.xmpp.register_handler(Callback(
"Message Retracted",
StanzaPath("message/apply_to/retract"),
StanzaPath("message/retract"),
self._handle_retract_message,
))
@@ -64,7 +64,6 @@ class XEP_0424(BasePlugin):
if include_fallback:
msg['body'] = fallback_text
msg.enable('fallback')
msg['apply_to']['id'] = id
msg['apply_to'].enable('retract')
msg['retract']['id'] = id
msg.enable('store')
msg.send()

View File

@@ -8,28 +8,27 @@ from slixmpp.xmlstream import (
ElementBase,
register_stanza_plugin,
)
from slixmpp.plugins.xep_0422.stanza import ApplyTo
from slixmpp.plugins.xep_0359 import OriginID
NS = 'urn:xmpp:message-retract:0'
NS = 'urn:xmpp:message-retract:1'
class Retract(ElementBase):
namespace = NS
name = 'retract'
plugin_attrib = 'retract'
interfaces = {'reason', 'id'}
sub_interfaces = {'reason'}
class Retracted(ElementBase):
namespace = NS
name = 'retracted'
plugin_attrib = 'retracted'
interfaces = {'stamp'}
interfaces = {'stamp', 'id', 'reason'}
sub_interfaces = {'reason'}
def register_plugins():
register_stanza_plugin(ApplyTo, Retract)
register_stanza_plugin(Message, Retract)
register_stanza_plugin(Message, Retracted)
register_stanza_plugin(Retracted, OriginID)

View File

@@ -13,10 +13,10 @@ from slixmpp.plugins.xep_0425 import stanza
class XEP_0425(BasePlugin):
'''XEP-0425: Message Moderation'''
'''XEP-0425: Moderated Message Retraction'''
name = 'xep_0425'
description = 'XEP-0425: Message Moderation'
description = 'XEP-0425: Moderated Message Retraction'
dependencies = {'xep_0424', 'xep_0421'}
stanza = stanza
namespace = stanza.NS
@@ -25,7 +25,7 @@ class XEP_0425(BasePlugin):
stanza.register_plugins()
self.xmpp.register_handler(Callback(
'Moderated Message',
StanzaPath('message/apply_to/moderated/retract'),
StanzaPath('message/retract/moderated'),
self._handle_moderated,
))
@@ -42,7 +42,7 @@ class XEP_0425(BasePlugin):
async def moderate(self, room: JID, id: str, reason: str = '', *,
ifrom: Optional[JID] = None, **iqkwargs):
iq = self.xmpp.make_iq_set(ito=room.bare, ifrom=ifrom)
iq['apply_to']['id'] = id
iq['apply_to']['moderate']['reason'] = reason
iq['apply_to']['moderate'].enable('retract')
iq['moderate']['id'] = id
iq['moderate']['reason'] = reason
iq['moderate'].enable('retract')
await iq.send(**iqkwargs)

View File

@@ -8,12 +8,11 @@ from slixmpp.xmlstream import (
ElementBase,
register_stanza_plugin,
)
from slixmpp.plugins.xep_0422.stanza import ApplyTo
from slixmpp.plugins.xep_0421.stanza import OccupantId
from slixmpp.plugins.xep_0424.stanza import Retract, Retracted
NS = 'urn:xmpp:message-moderate:0'
NS = 'urn:xmpp:message-moderate:1'
class Moderate(ElementBase):
@@ -28,17 +27,17 @@ class Moderated(ElementBase):
namespace = NS
name = 'moderated'
plugin_attrib = 'moderated'
interfaces = {'reason', 'by'}
sub_interfaces = {'reason'}
interfaces = {'by'}
def register_plugins():
register_stanza_plugin(Iq, ApplyTo)
register_stanza_plugin(ApplyTo, Moderate)
# for moderation requests
register_stanza_plugin(Iq, Moderate)
register_stanza_plugin(Moderate, Retract)
register_stanza_plugin(Message, Moderated)
register_stanza_plugin(ApplyTo, Moderated)
register_stanza_plugin(Moderated, Retract)
register_stanza_plugin(Moderated, Retracted)
# for moderation events
register_stanza_plugin(Retract, Moderated)
register_stanza_plugin(Moderated, OccupantId)
# for tombstones
register_stanza_plugin(Retracted, Moderated)

View File

@@ -1,8 +1,13 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2020 Mathieu Pasquet <mathieui@mathieui.net>
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
from abc import ABC
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from slixmpp.stanza import Message
from slixmpp.xmlstream import (
ElementBase,
@@ -10,14 +15,83 @@ from slixmpp.xmlstream import (
)
NS = 'urn:xmpp:fallback:0'
NS = "urn:xmpp:fallback:0"
class Fallback(ElementBase):
namespace = NS
name = 'fallback'
plugin_attrib = 'fallback'
name = "fallback"
plugin_attrib = "fallback"
plugin_multi_attrib = "fallbacks"
interfaces = {"for"}
def _find_fallback(self, fallback_for: str) -> "Fallback":
if self["for"] == fallback_for:
return self
for fallback in self.parent()["fallbacks"]:
if fallback["for"] == fallback_for:
return fallback
raise AttributeError("No fallback for this namespace", fallback_for)
def get_stripped_body(
self, fallback_for: str, element: Literal["body", "subject"] = "body"
) -> str:
"""
Get the body of a message, with the fallback part stripped
:param fallback_for: namespace of the fallback to strip
:param element: set this to "subject" get the stripped subject instead
of body
:return: body (or subject) content minus the fallback part
"""
fallback = self._find_fallback(fallback_for)
start = fallback[element]["start"]
end = fallback[element]["end"]
body = self.parent()[element]
if start == end == 0:
return ""
if start <= end < len(body):
return body[:start] + body[end:]
else:
return body
class FallbackMixin(ABC):
namespace = NS
name = NotImplemented
plugin_attrib = NotImplemented
interfaces = {"start", "end"}
def set_start(self, v: int):
self._set_attr("start", str(v))
def get_start(self):
return _int_or_zero(self._get_attr("start"))
def set_end(self, v: int):
self._set_attr("end", str(v))
def get_end(self):
return _int_or_zero(self._get_attr("end"))
class FallbackBody(FallbackMixin, ElementBase):
name = plugin_attrib = "body"
class FallbackSubject(FallbackMixin, ElementBase):
name = plugin_attrib = "subject"
def _int_or_zero(v: str):
try:
return int(v)
except ValueError:
return 0
def register_plugins():
register_stanza_plugin(Message, Fallback)
register_stanza_plugin(Message, Fallback, iterable=True)
register_stanza_plugin(Fallback, FallbackBody)
register_stanza_plugin(Fallback, FallbackSubject)

View File

@@ -1,3 +1,5 @@
from typing import Optional
from slixmpp.plugins import BasePlugin
from slixmpp.types import JidStr
from slixmpp.xmlstream import StanzaBase
@@ -13,7 +15,7 @@ class XEP_0461(BasePlugin):
name = "xep_0461"
description = "XEP-0461: Message Replies"
dependencies = {"xep_0030"}
dependencies = {"xep_0030", "xep_0428"}
stanza = stanza
namespace = stanza.NS
@@ -36,13 +38,35 @@ class XEP_0461(BasePlugin):
def _handle_reply_to_message(self, msg: StanzaBase):
self.xmpp.event("message_reply", msg)
def send_reply(self, reply_to: JidStr, reply_id: str, **msg_kwargs):
def make_reply(self, reply_to: JidStr, reply_id: str,
fallback: Optional[str] = None,
quoted_nick: Optional[str] = None, **msg_kwargs):
"""Create a replies message stanza
:param reply_to: Full JID of the quoted author
:param reply_id: ID of the message to reply to
:param fallback: Body of the quoted message
:param quoted_nick: nickname of the quoted participant
:param msg_kwargs: Parameters are consistent with the make_message method,
required parameters are ``mto`` and ``mbody``
"""
msg = self.xmpp.make_message(**msg_kwargs)
msg["reply"]["to"] = reply_to
msg["reply"]["id"] = reply_id
if fallback:
msg["reply"].add_quoted_fallback(fallback, quoted_nick)
return msg
def send_reply(self, reply_to: JidStr, reply_id: str,
fallback: Optional[str] = None,
quoted_nick: Optional[str] = None, **msg_kwargs):
"""
:param reply_to: Full JID of the quoted author
:param reply_id: ID of the message to reply to
:param fallback: Body of the quoted message
:param quoted_nick: nickname of the quoted participant
"""
msg = self.xmpp.make_message(**msg_kwargs)
msg["reply"]["to"] = reply_to
msg["reply"]["id"] = reply_id
msg = self.make_reply(reply_to, reply_id, fallback, quoted_nick, **msg_kwargs)
msg.send()

View File

@@ -1,5 +1,8 @@
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
from slixmpp.plugins.xep_0428.stanza import Fallback
NS = "urn:xmpp:reply:0"
@@ -10,71 +13,61 @@ class Reply(ElementBase):
plugin_attrib = "reply"
interfaces = {"id", "to"}
def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None):
"""
Add plain text fallback for clients not implementing XEP-0461.
class FeatureFallBack(ElementBase):
# should also be a multi attrib
namespace = "urn:xmpp:fallback:0"
name = "fallback"
plugin_attrib = "feature_fallback"
interfaces = {"for"}
``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will
prepend "> Bob:\n> Some text\n" to the body of the message, and set the
fallback_body attributes accordingly, so that clients implementing
XEP-0461 can hide the fallback text.
def get_fallback_body(self):
# only works for a single fallback_body attrib
start = self["fallback_body"]["start"]
end = self["fallback_body"]["end"]
body = self.parent()["body"]
:param fallback: Body of the quoted message.
:param nickname: Optional, nickname of the quoted participant.
"""
msg = self.parent()
quoted = "\n".join("> " + x.strip() for x in fallback.split("\n")) + "\n"
if nickname:
quoted = "> " + nickname + ":\n" + quoted
msg["body"] = quoted + msg["body"]
fallback_elem = Fallback()
fallback_elem["for"] = NS
fallback_elem["body"]["start"] = 0
fallback_elem["body"]["end"] = len(quoted)
msg.append(fallback_elem)
def get_fallback_body(self) -> str:
msg = self.parent()
for fallback in msg["fallbacks"]:
if fallback["for"] == NS:
break
else:
return ""
start = fallback["body"]["start"]
end = fallback["body"]["end"]
body = msg["body"]
if start <= end:
return body[start:end]
else:
return ""
def strip_fallback_content(self) -> str:
msg = self.parent()
for fallback in msg["fallbacks"]:
if fallback["for"] == NS:
break
else:
return msg["body"]
def get_stripped_body(self):
# only works for a single fallback_body attrib
start = self["fallback_body"]["start"]
end = self["fallback_body"]["end"]
body = self.parent()["body"]
if start <= end < len(body):
start = fallback["body"]["start"]
end = fallback["body"]["end"]
body = msg["body"]
if 0 <= start < end <= len(body):
return body[:start] + body[end:]
else:
return body
def add_quoted_fallback(self, fallback: str):
msg = self.parent()
quoted = "\n".join("> " + x.strip() for x in fallback.split("\n")) + "\n"
msg["body"] = quoted + msg["body"]
msg["feature_fallback"]["for"] = NS
msg["feature_fallback"]["fallback_body"]["start"] = 0
msg["feature_fallback"]["fallback_body"]["end"] = len(quoted)
class FallBackBody(ElementBase):
# According to https://xmpp.org/extensions/inbox/compatibility-fallback.html
# this should be a multi_attrib *but* since it's a protoXEP, we'll see...
namespace = FeatureFallBack.namespace
name = "body"
plugin_attrib = "fallback_body"
interfaces = {"start", "end"}
def set_start(self, v: int):
self._set_attr("start", str(v))
def get_start(self):
try:
return int(self._get_attr("start"))
except ValueError:
return 0
def set_end(self, v: int):
self._set_attr("end", str(v))
def get_end(self):
try:
return int(self._get_attr("end"))
except ValueError:
return 0
def register_plugins():
register_stanza_plugin(Message, Reply)
register_stanza_plugin(Message, FeatureFallBack)
register_stanza_plugin(FeatureFallBack, FallBackBody)

View File

@@ -0,0 +1,8 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .pinning import XEP_0469
register_plugin(XEP_0469)
__all__ = ['stanza', 'XEP_0469']

View File

@@ -0,0 +1,17 @@
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0469(BasePlugin):
"""
XEP-0469: Bookmark Pinning
"""
name = "xep_0469"
description = "XEP-0469: Bookmark Pinning"
dependencies = {"xep_0402"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@@ -0,0 +1,31 @@
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0402.stanza import Extensions
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:bookmarks-pinning:0"
class Pinned(ElementBase):
"""
Pinned bookmark element
To enable it on a Conference element, use enable() like this:
.. code-block::python
# C being a Conference element
C['extensions'].enable('pinned')
Which will add the <pinned> element to the <extensions> element.
"""
namespace = NS
name = "pinned"
plugin_attrib = "pinned"
interfaces = {"pinned"}
bool_interfaces = {"pinned"}
is_extension = True
def register_plugin():
register_stanza_plugin(Extensions, Pinned)

View File

@@ -0,0 +1,8 @@
from slixmpp.plugins.base import register_plugin
from . import stanza
from .mds import XEP_0490
register_plugin(XEP_0490)
__all__ = ['stanza', 'XEP_0490']

View File

@@ -0,0 +1,42 @@
from asyncio import Future
from slixmpp import Iq
from slixmpp.plugins import BasePlugin
from slixmpp.types import JidStr
from . import stanza
from ..xep_0004 import Form
class XEP_0490(BasePlugin):
"""
XEP-0490: Message Displayed Synchronization
"""
name = "xep_0490"
description = "XEP-0490: Message Displayed Synchronization"
dependencies = {"xep_0060", "xep_0163", "xep_0223", "xep_0359"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()
self.xmpp.plugin["xep_0163"].register_pep(
"message_displayed_synchronization",
stanza.Displayed,
)
self.xmpp.plugin["xep_0223"].node_profiles[self.stanza.NS] = {
"pubsub#max_items": "max",
"pubsub#send_last_published_item": "never",
}
def flag_chat(self, chat: JidStr, stanza_id: str, **kwargs) -> Future[Iq]:
displayed = stanza.Displayed()
displayed["stanza_id"]["id"] = stanza_id
return self.xmpp.plugin["xep_0223"].store(
displayed, node=stanza.NS, id=str(chat), **kwargs
)
def catch_up(self, **kwargs):
return self.xmpp.plugin["xep_0060"].get_items(
self.xmpp.boundjid.bare, stanza.NS, **kwargs
)

View File

@@ -0,0 +1,17 @@
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0060.stanza import Item
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0359.stanza import StanzaID
NS = "urn:xmpp:mds:displayed:0"
class Displayed(ElementBase):
namespace = NS
name = "displayed"
plugin_attrib = "displayed"
def register_plugin():
register_stanza_plugin(Displayed, StanzaID)
register_stanza_plugin(Item, Displayed)

View File

@@ -69,12 +69,14 @@ from slixmpp.plugins.xep_0249 import XEP_0249
from slixmpp.plugins.xep_0256 import XEP_0256
from slixmpp.plugins.xep_0257 import XEP_0257
from slixmpp.plugins.xep_0258 import XEP_0258
from slixmpp.plugins.xep_0264 import XEP_0264
from slixmpp.plugins.xep_0279 import XEP_0279
from slixmpp.plugins.xep_0280 import XEP_0280
from slixmpp.plugins.xep_0297 import XEP_0297
from slixmpp.plugins.xep_0300 import XEP_0300
from slixmpp.plugins.xep_0308 import XEP_0308
from slixmpp.plugins.xep_0313 import XEP_0313
from slixmpp.plugins.xep_0317 import XEP_0317
from slixmpp.plugins.xep_0319 import XEP_0319
from slixmpp.plugins.xep_0332 import XEP_0332
from slixmpp.plugins.xep_0333 import XEP_0333
@@ -100,6 +102,8 @@ from slixmpp.plugins.xep_0428 import XEP_0428
from slixmpp.plugins.xep_0437 import XEP_0437
from slixmpp.plugins.xep_0439 import XEP_0439
from slixmpp.plugins.xep_0444 import XEP_0444
from slixmpp.plugins.xep_0461 import XEP_0461
from slixmpp.plugins.xep_0490 import XEP_0490
class PluginsDict(TypedDict):
@@ -162,12 +166,14 @@ class PluginsDict(TypedDict):
xep_0256: XEP_0256
xep_0257: XEP_0257
xep_0258: XEP_0258
xep_0264: XEP_0264
xep_0279: XEP_0279
xep_0280: XEP_0280
xep_0297: XEP_0297
xep_0300: XEP_0300
xep_0308: XEP_0308
xep_0313: XEP_0313
xep_0317: XEP_0317
xep_0319: XEP_0319
xep_0332: XEP_0332
xep_0333: XEP_0333
@@ -193,3 +199,5 @@ class PluginsDict(TypedDict):
xep_0437: XEP_0437
xep_0439: XEP_0439
xep_0444: XEP_0444
xep_0461: XEP_0461
xep_0490: XEP_0490

View File

@@ -29,9 +29,9 @@ class SlixIntegration(IsolatedAsyncioTestCase):
self.clients = []
self.addAsyncCleanup(self._destroy)
def envjid(self, name):
def envjid(self, name: str, *, default: Optional[str] = None) -> JID:
"""Get a JID from an env var"""
value = os.getenv(name)
value = os.getenv(name, default=default)
return JID(value)
def envstr(self, name):

View File

@@ -3,6 +3,7 @@
# Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import atexit
import unittest
from queue import Queue
from xml.parsers.expat import ExpatError
@@ -750,3 +751,12 @@ class SlixTest(unittest.TestCase):
Error.namespace = 'jabber:client'
for st in Message, Iq, Presence:
register_stanza_plugin(st, Error)
@atexit.register
def cleanup():
try:
loop = asyncio.get_event_loop()
loop.close()
except:
pass

View File

@@ -53,17 +53,20 @@ MucAffiliation = Literal[
'outcast', 'member', 'admin', 'owner', 'none'
]
OptJid = Optional[JID]
JidStr = Union[str, JID]
OptJidStr = Optional[Union[str, JID]]
class PresenceArgs(TypedDict, total=False):
pfrom: JID
pto: JID
pfrom: JidStr
pto: JidStr
pshow: PresenceShows
ptype: PresenceTypes
pstatus: str
class MucRoomItem(TypedDict, total=False):
jid: JID
jid: str
role: MucRole
affiliation: MucAffiliation
show: Optional[PresenceShows]
@@ -75,10 +78,6 @@ MucRoomItemKeys = Literal[
'jid', 'role', 'affiliation', 'show', 'status', 'alt_nick',
]
OptJid = Optional[JID]
JidStr = Union[str, JID]
OptJidStr = Optional[Union[str, JID]]
MAMDefault = Literal['always', 'never', 'roster']
FilterString = Literal['in', 'out', 'out_sync']

View File

@@ -181,7 +181,7 @@ class SCRAM(Mech):
channel_binding = True
required_credentials = {'username', 'password'}
optional_credentials = {'authzid', 'channel_binding'}
security = {'encrypted', 'unencrypted_scram'}
security = {'tls_version', 'encrypted', 'unencrypted_scram', 'binding_proposed'}
def setup(self, name):
self.use_channel_binding = False
@@ -244,11 +244,15 @@ class SCRAM(Mech):
self.cnonce = bytes(('%s' % random.random())[2:])
gs2_cbind_flag = b'n'
if self.credentials['channel_binding']:
if self.use_channel_binding:
gs2_cbind_flag = b'p=tls-unique'
else:
gs2_cbind_flag = b'y'
if self.security_settings['binding_proposed']:
if self.credentials['channel_binding'] and \
self.use_channel_binding:
if self.security_settings['tls_version'] != 'TLSv1.3':
gs2_cbind_flag = b'p=tls-unique'
else:
gs2_cbind_flag = b'p=tls-exporter'
else:
gs2_cbind_flag = b'y'
authzid = b''
if self.credentials['authzid']:
@@ -280,7 +284,7 @@ class SCRAM(Mech):
raise SASLCancelled('Invalid nonce')
cbind_data = b''
if self.use_channel_binding:
if self.use_channel_binding and self.credentials['channel_binding']:
cbind_data = self.credentials['channel_binding']
cbind_input = self.gs2_header + cbind_data
channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')

View File

@@ -5,5 +5,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
__version__ = '1.8.4'
__version_info__ = (1, 8, 4)
__version__ = '1.8.6'
__version_info__ = (1, 8, 6)

View File

@@ -10,5 +10,5 @@ from slixmpp.xmlstream.tostring import tostring, highlight
from slixmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
__all__ = ['JID', 'StanzaBase', 'ElementBase',
'ET', 'StateMachine', 'tostring', 'highlight', 'XMLStream',
'RESPONSE_TIMEOUT']
'ET', 'tostring', 'highlight', 'XMLStream',
'RESPONSE_TIMEOUT', 'register_stanza_plugin']

View File

@@ -732,6 +732,9 @@ class ElementBase(object):
return plugin[full_attrib]
return plugin
else:
# XXX: This is legacy from SleekXMPP
# We've probably missed the opportunity to fix it
logging.warning("Unknown stanza interface: %s" % full_attrib)
return ''
def __setitem__(self, attrib: str, value: Any) -> Any:
@@ -1230,7 +1233,7 @@ class ElementBase(object):
if type(item) == XML_TYPE:
return self.appendxml(item)
else:
raise TypeError
raise TypeError(f"Cannot append {item!r} to a stanza")
self.xml.append(item.xml)
if item.__class__ == self.plugin_tag_map.get(item.tag_name(), None):
self.init_plugin(item.plugin_attrib,
@@ -1243,7 +1246,7 @@ class ElementBase(object):
self.init_plugin(item.__class__.plugin_multi_attrib)
else:
self.iterables.append(item)
item.parent = weakref.ref(self)
return self
def appendxml(self, xml: ET.Element) -> ElementBase:

View File

@@ -290,8 +290,8 @@ class XMLStream(asyncio.BaseProtocol):
self.xml_depth = 0
self.xml_root = None
self.force_starttls = None
self.disable_starttls = None
self.force_starttls = True
self.disable_starttls = False
self.waiting_queue = asyncio.Queue()
@@ -405,8 +405,9 @@ class XMLStream(asyncio.BaseProtocol):
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = False,
force_starttls: Optional[bool] = True, disable_starttls: Optional[bool] = False) -> None:
def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> None:
"""Create a new socket and connect to the server.
:param host: The name of the desired server for the connection.
@@ -523,7 +524,7 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.loop.run_until_complete(self.disconnected)
else:
tasks: List[Awaitable] = [asyncio.sleep(timeout)]
tasks: List[Union[asyncio.Task, asyncio.Future]] = [asyncio.Task(asyncio.sleep(timeout))]
if not forever:
tasks.append(self.disconnected)
self.loop.run_until_complete(asyncio.wait(tasks))
@@ -849,6 +850,8 @@ class XMLStream(asyncio.BaseProtocol):
log.debug("Connection error:", exc_info=True)
self.disconnect()
return False
if transp is None:
raise Exception("Transport should not be none")
der_cert = transp.get_extra_info("ssl_object").getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
self.event('ssl_cert', pem_cert)
@@ -1347,6 +1350,7 @@ class XMLStream(asyncio.BaseProtocol):
if isinstance(data, (RootStanza, str)) and not passthrough:
self.__queued_stanzas.append((data, use_filters))
log.debug('NOT SENT: %s %s', type(data), data)
self.event('stanza_not_sent', data)
return
self.waiting_queue.put_nowait((data, use_filters))
@@ -1410,7 +1414,11 @@ class XMLStream(asyncio.BaseProtocol):
# Convert the raw XML object into a stanza object. If no registered
# stanza type applies, a generic StanzaBase stanza will be used.
stanza: Optional[StanzaBase] = self._build_stanza(xml)
try:
stanza: Optional[StanzaBase] = self._build_stanza(xml)
except Exception as exc:
log.exception("Unable to parse stanza: %s,\n%s", exc, xml)
stanza = None
for filter in self.__filters['in']:
if stanza is not None:
filter = cast(SyncFilter, filter)

View File

@@ -0,0 +1,67 @@
import unittest
from slixmpp import Presence
from slixmpp.test import SlixTest
import slixmpp.plugins.xep_0317 as xep_0317
from slixmpp.plugins.xep_0317 import stanza
class TestStanzaHats(SlixTest):
def setUp(self):
stanza.register_plugin()
def test_create_hats(self):
raw_xml = """
<hats xmlns="urn:xmpp:hats:0">
<hat uri="http://example.com/hats#Teacher" title="Teacher"/>
</hats>
"""
hats = xep_0317.Hats()
hat = xep_0317.Hat()
hat['uri'] = 'http://example.com/hats#Teacher'
hat['title'] = 'Teacher'
hats.append(hat)
self.check(hats, raw_xml, use_values=False)
def test_set_single_hat(self):
presence = Presence()
presence["hats"]["hat"]["uri"] = "test-uri"
presence["hats"]["hat"]["title"] = "test-title"
self.check(
presence, # language=XML
"""
<presence>
<hats xmlns='urn:xmpp:hats:0'>
<hat uri='test-uri' title='test-title'/>
</hats>
</presence>
""",
)
def test_set_multi_hat(self):
presence = Presence()
presence["hats"].add_hats([("uri1", "title1"), ("uri2", "title2")])
self.check(
presence, # language=XML
"""
<presence>
<hats xmlns='urn:xmpp:hats:0'>
<hat uri='uri1' title='title1'/>
<hat uri='uri2' title='title2'/>
</hats>
</presence>
""",
)
def test_get_hats(self):
presence = Presence()
presence["hats"].add_hats([("uri1", "title1"), ("uri2", "title2")])
for i, hat in enumerate(presence["hats"]["hats"], start=1):
self.assertEqual(hat["uri"], f"uri{i}")
self.assertEqual(hat["title"], f"title{i}")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaHats)

View File

@@ -1,9 +1,7 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0356 import stanza
from slixmpp.plugins.xep_0356 import stanza, permissions
class TestPermissions(SlixTest):
@@ -12,30 +10,57 @@ class TestPermissions(SlixTest):
def testAdvertisePermission(self):
xmlstring = """
<message from='capulet.net' to='pubub.capulet.lit'>
<message from='capulet.lit' to='pubsub.capulet.lit'>
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
<perm access='presence' type='managed_entity'/>
<perm access='iq' type='both'/>
</privilege>
</message>
"""
msg = self.Message()
msg["from"] = "capulet.net"
msg["to"] = "pubub.capulet.lit"
# This raises AttributeError: 'NoneType' object has no attribute 'use_origin_id'
# msg["id"] = "id"
msg["from"] = "capulet.lit"
msg["to"] = "pubsub.capulet.lit"
for access, type_ in [
("roster", "both"),
("message", "outgoing"),
("presence", "managed_entity"),
("roster", permissions.RosterAccess.BOTH),
("message", permissions.MessagePermission.OUTGOING),
("presence", permissions.PresencePermission.MANAGED_ENTITY),
("iq", permissions.IqPermission.BOTH),
]:
msg["privilege"].add_perm(access, type_)
self.check(msg, xmlstring)
# Should this one work? → # AttributeError: 'Message' object has no attribute 'permission'
# self.assertEqual(msg.permission["roster"], "both")
def testIqPermission(self):
x = stanza.Privilege()
x["access"] = "iq"
ns = stanza.NameSpace()
ns["ns"] = "some_ns"
ns["type"] = "get"
x["perm"]["access"] = "iq"
x["perm"].append(ns)
ns = stanza.NameSpace()
ns["ns"] = "some_other_ns"
ns["type"] = "both"
x["perm"].append(ns)
self.check(
x,
"""
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='iq'>
<namespace ns='some_ns' type='get' />
<namespace ns='some_other_ns' type='both' />
</perm>
</privilege>
"""
)
nss = set()
for perm in x["perms"]:
for ns in perm["namespaces"]:
nss.add((ns["ns"], ns["type"]))
assert nss == {("some_ns", "get"), ("some_other_ns", "both")}
suite = unittest.TestLoader().loadTestsFromTestCase(TestPermissions)

View File

@@ -2,38 +2,33 @@ import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0424 import stanza
from slixmpp.plugins.xep_0422 import stanza as astanza
class TestRetraction(SlixTest):
def setUp(self):
astanza.register_plugins()
stanza.register_plugins()
def testRetract(self):
message = Message()
message['apply_to']['id'] = 'some-id'
message['apply_to']['retract']
message['retract']['id'] = 'some-id'
self.check(message, """
<message>
<apply-to xmlns="urn:xmpp:fasten:0" id="some-id">
<retract xmlns="urn:xmpp:message-retract:0"/>
</apply-to>
<retract xmlns="urn:xmpp:message-retract:1" id="some-id"/>
</message>
""", use_values=False)
def testRetracted(self):
message = Message()
message['retracted']['stamp'] = '2019-09-20T23:09:32Z'
message['retracted']['origin_id']['id'] = 'originid'
message['retracted']['id'] = 'originid'
self.check(message, """
<message>
<retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0">
<origin-id xmlns="urn:xmpp:sid:0" id="originid"/>
</retracted>
<retracted stamp="2019-09-20T23:09:32Z"
xmlns="urn:xmpp:message-retract:1"
id="originid" />
</message>
""")

View File

@@ -1,45 +1,48 @@
import unittest
from slixmpp import Message, Iq, JID
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0424 import stanza as stanza424
from slixmpp.plugins.xep_0425 import stanza
class TestModeration(SlixTest):
def setUp(self):
stanza424.register_plugins()
stanza.register_plugins()
def testModerate(self):
iq = Iq()
iq['type'] = 'set'
iq['id'] = 'a'
iq['apply_to']['id'] = 'some-id'
iq['apply_to']['moderate'].enable('retract')
iq['apply_to']['moderate']['reason'] = 'R'
iq['moderate']['id'] = 'some-id'
iq['moderate'].enable('retract')
iq['moderate']['reason'] = 'R'
self.check(iq, """
<iq type='set' id='a'>
<apply-to id="some-id" xmlns="urn:xmpp:fasten:0">
<moderate xmlns='urn:xmpp:message-moderate:0'>
<retract xmlns='urn:xmpp:message-retract:0'/>
<reason>R</reason>
</moderate>
</apply-to>
<moderate xmlns='urn:xmpp:message-moderate:1'>
<retract xmlns='urn:xmpp:message-retract:1'/>
<reason>R</reason>
</moderate>
</iq>
""", use_values=False)
def testModerated(self):
message = Message()
message['moderated']['by'] = JID('toto@titi')
message['moderated']['retracted']['stamp'] = '2019-09-20T23:09:32Z'
message['moderated']['reason'] = 'R'
message['retract']['id'] = 'some-id'
message['retract']['moderated']['by'] = JID('toto@titi')
message['retract']['moderated']['occupant-id']['id'] = 'oc-id'
message['retract']['reason'] = 'R'
self.check(message, """
<message>
<moderated xmlns="urn:xmpp:message-moderate:0" by="toto@titi">
<retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0" />
<retract id='some-id' xmlns='urn:xmpp:message-retract:1'>
<moderated by='toto@titi' xmlns='urn:xmpp:message-moderate:1'>
<occupant-id xmlns="urn:xmpp:occupant-id:0" id="oc-id" />
</moderated>
<reason>R</reason>
</moderated>
</retract>
</message>
""")

View File

@@ -0,0 +1,149 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0428 import stanza
from slixmpp.plugins import xep_0461
from slixmpp.plugins import xep_0444
class TestFallback(SlixTest):
def setUp(self):
stanza.register_plugins()
def testSingleFallbackBody(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 8
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body start="0" end="8" />
</fallback>
</message>
""",
)
def testSingleFallbackSubject(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"]["subject"]["start"] = 0
message["fallback"]["subject"]["end"] = 8
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<subject start="0" end="8" />
</fallback>
</message>
""",
)
def testSingleFallbackWholeBody(self):
message = Message()
message["fallback"]["for"] = "ns"
message["fallback"].enable("body")
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body />
</fallback>
</message>
""",
)
def testMultiFallback(self):
message = Message()
f1 = stanza.Fallback()
f1["for"] = "ns1"
f2 = stanza.Fallback()
f2["for"] = "ns2"
message.append(f1)
message.append(f2)
self.check(
message, # language=XML
"""
<message>
<fallback xmlns='urn:xmpp:fallback:0' for='ns1' />
<fallback xmlns='urn:xmpp:fallback:0' for='ns2' />
</message>
""",
)
for i, fallback in enumerate(message["fallbacks"], start=1):
self.assertEqual(fallback["for"], f"ns{i}")
def testStripFallbackPartOfBody(self):
message = Message()
message["body"] = "> quoted\nsome-body"
message["fallback"]["for"] = xep_0461.stanza.NS
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 9
self.check(
message, # language=XML
"""
<message>
<body>&gt; quoted\nsome-body</body>
<fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="9" />
</fallback>
</message>
""",
)
self.assertEqual(
message["fallback"].get_stripped_body(xep_0461.stanza.NS), "some-body"
)
def testStripWholeBody(self):
message = Message()
message["body"] = "> quoted\nsome-body"
message["fallback"]["for"] = "ns"
message["fallback"].enable("body")
self.check(
message, # language=XML
"""
<message>
<body>&gt; quoted\nsome-body</body>
<fallback xmlns='urn:xmpp:fallback:0' for='ns'>
<body />
</fallback>
</message>
""",
)
self.assertEqual(message["fallback"].get_stripped_body("ns"), "")
def testStripMultiFallback(self):
message = Message()
message["body"] = "> huuuuu\n👍"
message["fallback"]["for"] = xep_0461.stanza.NS
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 9
reaction_fallback = stanza.Fallback()
reaction_fallback["for"] = xep_0444.stanza.NS
reaction_fallback.enable("body")
message.append(reaction_fallback)
self.assertEqual(message["fallback"].get_stripped_body(xep_0461.stanza.NS), "👍")
self.assertEqual(message["fallback"].get_stripped_body(xep_0444.stanza.NS), "")
suite = unittest.TestLoader().loadTestsFromTestCase(TestFallback)

View File

@@ -1,11 +1,13 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0428 import stanza as fallback_stanza
from slixmpp.plugins.xep_0461 import stanza
class TestReply(SlixTest):
def setUp(self):
fallback_stanza.register_plugins()
stanza.register_plugins()
def testReply(self):
@@ -26,9 +28,9 @@ class TestReply(SlixTest):
def testFallback(self):
message = Message()
message["body"] = "12345\nrealbody"
message["feature_fallback"]["for"] = "NS"
message["feature_fallback"]["fallback_body"]["start"] = 0
message["feature_fallback"]["fallback_body"]["end"] = 6
message["fallback"]["for"] = "NS"
message["fallback"]["body"]["start"] = 0
message["fallback"]["body"]["end"] = 6
self.check(
message,
@@ -42,18 +44,18 @@ class TestReply(SlixTest):
""",
)
assert message["feature_fallback"].get_stripped_body() == "realbody"
assert message["fallback"].get_stripped_body("NS") == "realbody"
def testAddFallBackHelper(self):
msg = Message()
msg["body"] = "Great"
msg["feature_fallback"].add_quoted_fallback("Anna wrote:\nHi, how are you?")
# ugly dedent but the test does not pass without it
msg["reply"].add_quoted_fallback("Anna wrote:\nHi, how are you?")
self.check(
msg,
msg, # language=XML
"""
<message xmlns="jabber:client" type="normal">
<body>> Anna wrote:\n> Hi, how are you?\nGreat</body>
<reply xmlns="urn:xmpp:reply:0" />
<fallback xmlns="urn:xmpp:fallback:0" for="urn:xmpp:reply:0">
<body start='0' end='33' />
</fallback>
@@ -67,8 +69,8 @@ class TestReply(SlixTest):
msg = Message()
msg["body"] = "Great"
msg["feature_fallback"].add_quoted_fallback(body)
body2 = msg["feature_fallback"].get_fallback_body()
msg["reply"].add_quoted_fallback(body)
body2 = msg["reply"].get_fallback_body()
self.assertTrue(body2 == quoted, body2)

View File

@@ -0,0 +1,36 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0469 import stanza
from slixmpp.plugins.xep_0402 import stanza as b_stanza
class TestBookmarksPinning(SlixTest):
def setUp(self):
b_stanza.register_plugin()
stanza.register_plugin()
def test_pinned(self):
bookmark = b_stanza.Conference()
bookmark["password"] = "pass"
bookmark["nick"] = "nick"
bookmark["autojoin"] = False
bookmark["extensions"].enable("pinned")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'
autojoin='false'>
<nick>nick</nick>
<password>pass</password>
<extensions>
<pinned xmlns="urn:xmpp:bookmarks-pinning:0" />
</extensions>
</conference>
""",
use_values=False
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestBookmarksPinning)

View File

@@ -0,0 +1,76 @@
import logging
import unittest
from slixmpp.test import SlixTest
class TestCaps(SlixTest):
def setUp(self):
self.stream_start(plugins=["xep_0115"])
def testConcurrentSameHash(self):
"""
Check that we only resolve a given ver string to a disco info once,
even if we receive several presences with that same ver string
consecutively.
"""
self.recv( # language=XML
"""
<presence from='romeo@montague.lit/orchard'>
<c xmlns='http://jabber.org/protocol/caps'
hash='sha-1'
node='a-node'
ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/>
</presence>
"""
)
self.recv( # language=XML
"""
<presence from='i-dont-know-much-shakespeare@montague.lit/orchard'>
<c xmlns='http://jabber.org/protocol/caps'
hash='sha-1'
node='a-node'
ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/>
</presence>
"""
)
self.send( # language=XML
"""
<iq xmlns="jabber:client"
id="1"
to="romeo@montague.lit/orchard"
type="get">
<query xmlns="http://jabber.org/protocol/disco#info"
node="a-node#h0TdMvqNR8FHUfFG1HauOLYZDqE="/>
</iq>
"""
)
self.send(None)
self.recv( # language=XML
"""
<iq from='romeo@montague.lit/orchard'
id='1'
type='result'>
<query xmlns='http://jabber.org/protocol/disco#info'
node='a-nodes#h0TdMvqNR8FHUfFG1HauOLYZDqE='>
<identity category='client' name='a client' type='pc'/>
<feature var='http://jabber.org/protocol/caps'/>
</query>
</iq>
"""
)
self.send(None)
self.assertTrue(
self.xmpp["xep_0030"].supports(
"romeo@montague.lit/orchard", "http://jabber.org/protocol/caps"
)
)
self.assertTrue(
self.xmpp["xep_0030"].supports(
"i-dont-know-much-shakespeare@montague.lit/orchard",
"http://jabber.org/protocol/caps",
)
)
logging.basicConfig(level=logging.DEBUG)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCaps)

View File

@@ -1,7 +1,7 @@
import unittest
from slixmpp import ComponentXMPP, Iq, Message
from slixmpp.roster import RosterItem
from slixmpp import Message, JID, Iq
from slixmpp.plugins.xep_0356 import permissions
from slixmpp.test import SlixTest
@@ -9,9 +9,9 @@ class TestPermissions(SlixTest):
def setUp(self):
self.stream_start(
mode="component",
plugins=["xep_0356"],
plugins=["xep_0356", "xep_0045"],
jid="pubsub.capulet.lit",
server="capulet.net",
server="capulet.lit",
)
def testPluginEnd(self):
@@ -23,26 +23,44 @@ class TestPermissions(SlixTest):
self.assertFalse(exc)
def testGrantedPrivileges(self):
# https://xmpp.org/extensions/xep-0356.html#example-4
results = {"event": False}
x = self.xmpp["xep_0356"]
self.xmpp.add_event_handler(
"privileges_advertised", lambda msg: results.__setitem__("event", True)
)
self.recv(
"""
<message from='capulet.net' to='pubub.capulet.lit' id='54321'>
<message from='capulet.lit' to='pubsub.capulet.lit' id='54321'>
<privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
<perm access='iq'>
<namespace ns='some_ns' type='get' />
<namespace ns='some_other_ns' type='both' />
</perm>
</privilege>
</message>
"""
)
self.assertEqual(self.xmpp["xep_0356"].granted_privileges["roster"], "both")
server = JID("capulet.lit")
self.assertEqual(
self.xmpp["xep_0356"].granted_privileges["message"], "outgoing"
x.granted_privileges[server].roster, permissions.RosterAccess.BOTH
)
self.assertEqual(
x.granted_privileges[server].message, permissions.MessagePermission.OUTGOING
)
self.assertEqual(
x.granted_privileges[server].presence, permissions.PresencePermission.NONE
)
self.assertEqual(
x.granted_privileges[server].iq["nope"], permissions.IqPermission.NONE
)
self.assertEqual(
x.granted_privileges[server].iq["some_ns"], permissions.IqPermission.GET
)
self.assertEqual(
x.granted_privileges[server].iq["some_other_ns"], permissions.IqPermission.BOTH
)
self.assertEqual(self.xmpp["xep_0356"].granted_privileges["presence"], "none")
self.assertTrue(results["event"])
def testGetRosterIq(self):
@@ -94,7 +112,7 @@ class TestPermissions(SlixTest):
def testMakeOutgoingMessage(self):
xmlstring = """
<message xmlns="jabber:component:accept" from='pubsub.capulet.lit' to='capulet.net'>
<message xmlns="jabber:component:accept" from='pubsub.capulet.lit' to='capulet.lit'>
<privilege xmlns='urn:xmpp:privilege:2'>
<forwarded xmlns='urn:xmpp:forward:0'>
<message from="juliet@capulet.lit" to="romeo@montague.lit" xmlns="jabber:client">
@@ -108,9 +126,49 @@ class TestPermissions(SlixTest):
msg["from"] = "juliet@capulet.lit"
msg["to"] = "romeo@montague.lit"
msg["body"] = "I do not hate you"
priv_msg = self.xmpp["xep_0356"]._make_privileged_message(msg)
self.check(priv_msg, xmlstring, use_values=False)
def testDetectServer(self):
msg = Message()
msg["from"] = "juliet@something"
msg["to"] = "romeo@montague.lit"
msg["body"] = "I do not hate you"
priv_msg = self.xmpp["xep_0356"]._make_privileged_message(msg)
assert priv_msg.get_to() == "something"
assert priv_msg.get_from() == "pubsub.capulet.lit"
def testIqOnBehalf(self):
iq = Iq()
iq["mucadmin_query"]["item"]["affiliation"] = "member"
iq.set_from("juliet@xxx")
iq.set_to("somemuc@conf")
iq.set_type("get")
self.xmpp["xep_0356"].granted_privileges["conf"].iq["http://jabber.org/protocol/muc#admin"] = permissions.IqPermission.BOTH
r = self.xmpp.loop.create_task(self.xmpp["xep_0356"].send_privileged_iq(iq, iq_id="0"))
self.send(
"""
<iq from="pubsub.capulet.lit"
to="juliet@xxx"
xmlns="jabber:component:accept"
type="get" id="0">
<privileged_iq xmlns='urn:xmpp:privilege:2'>
<iq xmlns='jabber:client'
type='get'
to='somemuc@conf'
from='juliet@xxx'
id="0">
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item affiliation='member'/>
</query>
</iq>
</privileged_iq>
</iq>
""",
use_values=False
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestPermissions)

View File

@@ -8,12 +8,14 @@ class TestReply(SlixTest):
self.stream_start(plugins=["xep_0461"])
def testFallBackBody(self):
async def on_reply(msg):
start = msg["feature_fallback"]["fallback_body"]["start"]
end = msg["feature_fallback"]["fallback_body"]["end"]
def on_reply(msg):
start = msg["fallback"]["body"]["start"]
end = msg["fallback"]["body"]["end"]
self.xmpp["xep_0461"].send_reply(
reply_to=msg.get_from(),
reply_id=msg.get_id(),
fallback=msg["reply"].strip_fallback_content(),
quoted_nick="res",
mto="test@test.com",
mbody=f"{start} to {end}",
)
@@ -26,7 +28,7 @@ class TestReply(SlixTest):
<reply xmlns="urn:xmpp:reply:0" id="some-id" />
<body>&gt; quoted\nsome-body</body>
<fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="8" />
<body start="0" end="9" />
</fallback>
</message>
"""
@@ -34,8 +36,11 @@ class TestReply(SlixTest):
self.send(
"""
<message xmlns="jabber:client" to="test@test.com" type="normal">
<body>&gt; res:\n&gt; some-body\n0 to 9</body>
<reply xmlns="urn:xmpp:reply:0" id="other-id" to="from@from.com/res" />
<body>0 to 8</body>
<fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
<body start="0" end="19" />
</fallback>
</message>
"""
)

View File

@@ -0,0 +1,135 @@
import unittest.mock
from slixmpp.test import SlixTest
# from slixmpp.plugins import xep_0490
class TestMessageDisplaySynchronization(SlixTest):
def setUp(self):
self.stream_start(jid="juliet@capulet.lit", plugins={"xep_0490"})
def test_catch_up(self):
future = self.xmpp.plugin["xep_0490"].catch_up()
self.send( # language=XML
"""
<iq type="get" to="juliet@capulet.lit" id="1">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<items node="urn:xmpp:mds:displayed:0" />
</pubsub>
</iq>
"""
)
self.recv( # language=XML
"""
<iq type='result'
to='juliet@capulet.lit/balcony'
id='1'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<items node='urn:xmpp:mds:displayed:0'>
<item id='romeo@montegue.lit'>
<displayed xmlns='urn:xmpp:mds:displayed:0'>
<stanza-id xmlns='urn:xmpp:sid:0'
id='0f710f2b-52ed-4d52-b928-784dad74a52b'
by='juliet@capulet.lit'/>
</displayed>
</item>
<item id='example@conference.shakespeare.lit'>
<displayed xmlns='urn:xmpp:mds:displayed:0'>
<stanza-id xmlns='urn:xmpp:sid:0'
id='ca21deaf-812c-48f1-8f16-339a674f2864'
by='example@conference.shakespeare.lit'/>
</displayed>
</item>
</items>
</pubsub>
</iq>
"""
)
iq = future.result()
item = list(iq["pubsub"]["items"])
self.assertEqual(item[0]["id"], "romeo@montegue.lit")
self.assertEqual(
item[0]["displayed"]["stanza_id"]["id"],
"0f710f2b-52ed-4d52-b928-784dad74a52b",
)
self.assertEqual(item[1]["id"], "example@conference.shakespeare.lit")
self.assertEqual(
item[1]["displayed"]["stanza_id"]["id"],
"ca21deaf-812c-48f1-8f16-339a674f2864",
)
def test_flag_chat(self):
self.xmpp.plugin["xep_0490"].flag_chat(
"romeo@montegue.lit", "0f710f2b-52ed-4d52-b928-784dad74a52b"
)
self.send( # language=XML
"""
<iq type='set' id='1'>
<pubsub xmlns='http://jabber.org/protocol/pubsub'>
<publish node='urn:xmpp:mds:displayed:0'>
<item id='romeo@montegue.lit'>
<displayed xmlns='urn:xmpp:mds:displayed:0'>
<stanza-id xmlns='urn:xmpp:sid:0'
id="0f710f2b-52ed-4d52-b928-784dad74a52b" />
</displayed>
</item>
</publish>
<publish-options>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#publish-options</value>
</field>
<field var='pubsub#persist_items'>
<value>1</value>
</field>
<field var='pubsub#max_items'>
<value>max</value>
</field>
<field var='pubsub#send_last_published_item'>
<value>never</value>
</field>
<field var='pubsub#access_model'>
<value>whitelist</value>
</field>
</x>
</publish-options>
</pubsub>
</iq>
""",
use_values=False,
)
def test_notification(self):
handler = unittest.mock.Mock()
self.xmpp.add_event_handler(
"message_displayed_synchronization_publish", handler
)
self.recv( # language=XML
"""
<message from='juliet@capulet.lit' to='juliet@capulet.lit/balcony' type='headline' id='new-displayed-pep-event'>
<event xmlns='http://jabber.org/protocol/pubsub#event'>
<items node='urn:xmpp:mds:displayed:0'>
<item id='romeo@montegue.lit'>
<displayed xmlns='urn:xmpp:mds:displayed:0'>
<stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='0423e3a9-d516-493d-bb06-bee0e51ab9fb'/>
</displayed>
</item>
</items>
</event>
</message>
"""
)
handler.assert_called()
msg = handler.call_args[0][0]
self.assertEqual(
msg["pubsub_event"]["items"]["item"]["id"], "romeo@montegue.lit"
)
self.assertEqual(
msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"],
"0423e3a9-d516-493d-bb06-bee0e51ab9fb",
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageDisplaySynchronization)