Compare commits

..

No commits in common. "master" and "test-ci" have entirely different histories.

87 changed files with 334 additions and 2225 deletions

13
.github/pull_request_template.md vendored Normal file
View File

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

81
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,81 @@
stages:
- lint
- test
- trigger
mypy:
stage: lint
tags:
- docker
image: python:3
script:
- pip3 install mypy
- mypy slixmpp
test-3.7:
stage: test
tags:
- docker
image: python:3.7
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.10:
stage: test
tags:
- docker
image: python:3.10
script:
- apt update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.11:
stage: test
tags:
- docker
image: python:3.11
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test-3.12:
stage: test
tags:
- docker
image: python:3.12-rc
allow_failure: true
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp cryptography
- ./run_tests.py
test_integration:
stage: test
tags:
- docker
image: python:3
only:
variables:
- $CI_ACCOUNT1
- $CI_ACCOUNT2
script:
- apt-get update
- apt-get install -y python3 python3-pip cython3 gpg
- pip3 install emoji aiohttp aiodns
- ./run_integration_tests.py
trigger_poezio:
stage: trigger
tags:
- docker
image: curlimages/curl:7.79.1
script:
- curl --request POST -F token="$SLIXMPP_TRIGGER_TOKEN" -F ref=master https://lab.louiz.org/api/v4/projects/18/trigger/pipeline

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: python
python:
- "3.7"
- "3.8-dev"
install:
- "pip install ."
script: testall.py

View File

@ -1,6 +1,3 @@
when:
event: [ push, pull_request ]
steps: steps:
mypy: mypy:
image: python:3 image: python:3

View File

@ -1,20 +1,7 @@
when:
event: [ push, pull_request ]
steps: steps:
test_integration: test_integration:
image: "python:3.11" image: "python:3.11"
environment: secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server]
CI_ACCOUNT1:
from_secret: ci_account1
CI_ACCOUNT1_PASSWORD:
from_secret: ci_account1_password
CI_ACCOUNT2:
from_secret: ci_account2
CI_ACCOUNT2_PASSWORD:
from_secret: ci_account2_password
CI_MUC_SERVER:
from_secret: ci_muc_server
commands: commands:
- apt-get update - apt-get update
- apt-get install -y python3-pip cython3 gpg idn libidn-dev - apt-get install -y python3-pip cython3 gpg idn libidn-dev

View File

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

View File

@ -616,14 +616,6 @@
<xmpp:since>1.0</xmpp:since> <xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0264.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.2</xmpp:version>
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0270.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0270.html"/>
@ -690,14 +682,6 @@
<xmpp:since>1.0</xmpp:since> <xmpp:since>1.0</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0317.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.8.6</xmpp:version>
<xmpp:since>0.2</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
@ -872,7 +856,7 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.0</xmpp:version> <xmpp:version>0.3.0</xmpp:version>
<xmpp:since>1.6.0</xmpp:since> <xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
@ -880,7 +864,7 @@
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/>
<xmpp:status>complete</xmpp:status> <xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version> <xmpp:version>0.2.1</xmpp:version>
<xmpp:since>1.6.0</xmpp:since> <xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
@ -916,53 +900,13 @@
<xmpp:since>1.6.0</xmpp:since> <xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements> <implements>
<xmpp:SupportedXep> <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
<xmpp:status>partial</xmpp:status> <xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version> <xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.1</xmpp:since> <xmpp:since>1.8.1</xmpp:since>
<xmpp:note>no thumbnail support</xmpp:note> <xmpp:note>no thumbnail support</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0469.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.6</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0482.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</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>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0492.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
<xmpp:since>1.8.7</xmpp:since>
</xmpp:SupportedXep> </xmpp:SupportedXep>
</implements> </implements>
@ -1127,12 +1071,5 @@
<file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/> <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/>
</Version> </Version>
</release> </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> </Project>
</rdf:RDF> </rdf:RDF>

View File

@ -17,7 +17,6 @@ Plugin index
xep_0049 xep_0049
xep_0050 xep_0050
xep_0054 xep_0054
xep_0055
xep_0059 xep_0059
xep_0060 xep_0060
xep_0065 xep_0065
@ -32,7 +31,6 @@ Plugin index
xep_0085 xep_0085
xep_0086 xep_0086
xep_0092 xep_0092
xep_0100
xep_0106 xep_0106
xep_0107 xep_0107
xep_0108 xep_0108
@ -64,15 +62,12 @@ Plugin index
xep_0256 xep_0256
xep_0257 xep_0257
xep_0258 xep_0258
xep_0264
xep_0279 xep_0279
xep_0280 xep_0280
xep_0292
xep_0297 xep_0297
xep_0300 xep_0300
xep_0308 xep_0308
xep_0313 xep_0313
xep_0317
xep_0319 xep_0319
xep_0332 xep_0332
xep_0333 xep_0333
@ -84,13 +79,9 @@ Plugin index
xep_0359 xep_0359
xep_0363 xep_0363
xep_0369 xep_0369
xep_0372
xep_0377 xep_0377
xep_0380 xep_0380
xep_0382
xep_0385
xep_0394 xep_0394
xep_0402
xep_0403 xep_0403
xep_0404 xep_0404
xep_0405 xep_0405
@ -103,9 +94,3 @@ Plugin index
xep_0439 xep_0439
xep_0441 xep_0441
xep_0444 xep_0444
xep_0446
xep_0447
xep_0461
xep_0469
xep_0490
xep_0492

View File

@ -1,5 +1,5 @@
XEP-0100: Gateway interaction XEP-0106: Gateway interaction
============================= =============================
.. module:: slixmpp.plugins.xep_0100 .. module:: slixmpp.plugins.xep_0100

View File

@ -1,18 +0,0 @@
XEP-0264: Jingle Content Thumbnails
===================================
.. module:: slixmpp.plugins.xep_0264
.. autoclass:: XEP_0264
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0264.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0317: Hats
==============
.. module:: slixmpp.plugins.xep_0317
.. autoclass:: XEP_0317
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0317.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0372: References
====================
.. module:: slixmpp.plugins.xep_0372
.. autoclass:: XEP_0372
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0372.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0382: Spoiler Messages
==========================
.. module:: slixmpp.plugins.xep_0382
.. autoclass:: XEP_0382
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0382.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0385: Stateless Inline Media Sharing (SIMS)
===============================================
.. module:: slixmpp.plugins.xep_0385
.. autoclass:: XEP_0385
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0385.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0402: PEP Native Bookmarks
==============================
.. module:: slixmpp.plugins.xep_0402
.. autoclass:: XEP_0402
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0402.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0446: File metadata element
===============================
.. module:: slixmpp.plugins.xep_0446
.. autoclass:: XEP_0446
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0446.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0447: Stateless File Sharing
================================
.. module:: slixmpp.plugins.xep_0447
.. autoclass:: XEP_0447
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0447.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0461: Message Replies
=========================
.. module:: slixmpp.plugins.xep_0461
.. autoclass:: XEP_0461
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0461.stanza
:members:
:undoc-members:

View File

@ -1,17 +0,0 @@
XEP-0469: Bookmark Pinning
==========================
.. module:: slixmpp.plugins.xep_0469
.. autoclass:: XEP_0469
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0469.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0490: Message Displayed Synchronization
===========================================
.. module:: slixmpp.plugins.xep_0490
.. autoclass:: XEP_0490
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0490.stanza
:members:
:undoc-members:

View File

@ -1,18 +0,0 @@
XEP-0492: Chat Notification Settings
====================================
.. module:: slixmpp.plugins.xep_0492
.. autoclass:: XEP_0492
:members:
:exclude-members: session_bind, plugin_init, plugin_end
Stanza elements
---------------
.. automodule:: slixmpp.plugins.xep_0492.stanza
:members:
:undoc-members:

View File

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

View File

@ -1,8 +1,6 @@
Projects Using Slixmpp Projects Using Slixmpp
====================== ======================
This page enumerates software in the form of applications, bots and gateways utilizing the XMPP protocols with slixmpp.
Applications Applications
------------ ------------
@ -10,8 +8,7 @@ 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. 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>`__ - `Source <https://github.com/moparisthebest/sendxmpp-py>`_
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`__
Bots Bots
---- ----
@ -20,65 +17,63 @@ BotLogMauve
~~~~~~~~~~~ ~~~~~~~~~~~
XMPP bot which logs groupchat messages. Logs are in text format, with one file per day and per groupchat. 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>`__ - `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 LinkBot
~~~~~~~ ~~~~~~~
This bot reveals the title of any shared link in a groupchat for quick content insight. This bot reveals the title of any shared link in a groupchat for quick content insight.
- `Source <https://git.xmpp-it.net/mario/XMPPBot>`__ - `Source <https://git.xmpp-it.net/mario/XMPPBot>`_
llama-bot llama-bot
~~~~~~~~~ ~~~~~~~~~
Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it. Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it.
- `Source <https://github.com/decent-im/llama-bot>`__ - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Demo <xmpp:llama@decent.im?message>`__ - `Source <https://github.com/decent-im/llama-bot>`_
- `Demo <xmpp:llama@decent.im?message>`_
Morbot Morbot
~~~~~~ ~~~~~~
Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs. Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs.
- `Source <https://codeberg.org/TheCoffeMaker/Morbot>`__ - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/Morbot>`_
Slixfeed 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. 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>`__ - `Groupchat <xmpp:slixfeed@chat.woodpeckersnest.space?join>`_
- `Source <https://gitgud.io/sjehuda/slixfeed>`__ - `Source <https://gitgud.io/sjehuda/slixfeed>`_
sms4you 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. sms4you forwards messages from and to SMS and connects either with sms4you-xmpp or sms4you-email to choose the other mean of communication. Nice for receiving or sending SMS, independently from carrying a SIM card.
- `Homepage <https://sms4you-team.pages.debian.net/sms4you/>`__ - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://salsa.debian.org/sms4you-team/sms4you>`__ - `Homepage <https://sms4you-team.pages.debian.net/sms4you/>`_
- `Source <https://salsa.debian.org/sms4you-team/sms4you>`_
Stable Diffusion Stable Diffusion
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
XMPP bot that generates digital images from textual descriptions. XMPP bot that generates digital images from textual descriptions.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`__ - `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Source <https://www.nicoco.fr/blog/2022/08/31/xmpp-bot-stable-diffusion/>`__ - `Source <https://www.nicoco.fr/blog/2022/08/31/xmpp-bot-stable-diffusion/>`_
WhisperBot WhisperBot
~~~~~~~~~~ ~~~~~~~~~~
XMPP bot that transliterates audio messages using OpenAI's Whisper libraries. XMPP bot that transliterates audio messages using OpenAI's Whisper libraries.
- `Source <https://codeberg.org/TheCoffeMaker/WhisperBot>`__ - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_
- `Source <https://codeberg.org/TheCoffeMaker/WhisperBot>`_
XMPP MUC Message Gateway XMPP MUC Message Gateway
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
A multipurpose JSON forwarder microservice from HTTP POST to XMPP MUC room over TLSv1.2 with SliXMPP. 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>`__ - `Source <https://github.com/immanuelfodor/xmpp-muc-message-gateway>`_
Services Services
-------- --------
@ -87,14 +82,14 @@ AtomToPubsub
~~~~~~~~~~~~ ~~~~~~~~~~~~
AtomToPubsub is a simple Python script that parses Atom + RSS feeds and pushes the entries to a designated XMPP Pubsub Node. 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>`__ - `Groupchat <xmpp:movim@conference.movim.eu?join>`_
- `Source <https://github.com/imattau/atomtopubsub>`__ - `Source <https://github.com/imattau/atomtopubsub>`_
Slidge Slidge
~~~~~~ ~~~~~~
Slidge is a general purpose XMPP gateway framework in Python. Slidge is a general purpose XMPP gateway framework in Python.
- `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`__ - `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_
- `Homepage <https://slidge.im/core/>`__ - `Homepage <https://slidge.im/core/>`_
- `Source <https://sr.ht/~nicoco/slidge>`__ - `Source <https://sr.ht/~nicoco/slidge>`_

View File

@ -50,39 +50,10 @@ Running the event loop
only run for this amount of time, and if ``forever`` is False it will only run for this amount of time, and if ``forever`` is False it will
run until disconnection). run until disconnection).
This wrapper should be removed in slixmpp 1.9.0.
Therefore you can handle the event loop in any way you like Therefore you can handle the event loop in any way you like
instead of using ``process()``. instead of using ``process()``.
Using connect()
~~~~~~~~~~~~~~~
:meth:`.XMLStream.connect` schedules a lot of things in the background, but that
only holds true if the event loop is running!
That is why in all examples we usually call connect() right before calling
a `loop.run_…` function, or the deprecated `process()` function.
Using a different event loop
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Immediately upon XMPP object creation (`ClientXMPP` / `ComponentXMPP`) you
should sets its `loop` attribute to whatever you want, and ideally this
should work. This path is less tested, so it may break, if that is the case
please report a bug.
Any access to the `loop` attribute if not user-initialized will set it
to the default asyncio event loop by default.
.. warning::
If the loop attribute is modified at runtime, the application will probably
end up in an hybrid state and asyncio may complain loudly that things bound
to an event loop are being ran in another. Try to avoid that situation.
Examples Examples
~~~~~~~~ ~~~~~~~~
@ -102,11 +73,10 @@ callbacks while everything is not ready.
callback = lambda _: client.connected_event.set() callback = lambda _: client.connected_event.set()
client.add_event_handler('session_start', callback) client.add_event_handler('session_start', callback)
client.connect() client.connect()
loop = asyncio.get_event_loop()
loop.run_until_complete(event.wait()) loop.run_until_complete(event.wait())
# do some other stuff before running the event loop, e.g. # do some other stuff before running the event loop, e.g.
# loop.run_until_complete(httpserver.init()) # loop.run_until_complete(httpserver.init())
loop.run_forever() client.process()
Use with other asyncio-based libraries Use with other asyncio-based libraries
@ -136,7 +106,7 @@ a simple <message>.
client.add_event_handler('session_start', get_pythonorg) client.add_event_handler('session_start', get_pythonorg)
client.add_event_handler('session_start', get_asyncioorg) client.add_event_handler('session_start', get_asyncioorg)
client.connect() client.connect()
client.loop.run_until_complete(client.disconnected) client.process()
Blocking Iq Blocking Iq
@ -166,6 +136,6 @@ JID indicating its findings.
client = ExampleClient('jid@example', 'password') client = ExampleClient('jid@example', 'password')
client.connect() client.connect()
client.loop.run_until_complete(client.disconnected) client.process()

View File

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

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

@ -23,6 +23,7 @@ class TestRetract(SlixIntegration):
fallback_text='Twas a mistake', fallback_text='Twas a mistake',
) )
msg = await self.clients[1].wait_until('message_retract') msg = await self.clients[1].wait_until('message_retract')
self.assertEqual(msg['retract']['id'], 'toto') self.assertEqual(msg['apply_to']['id'], 'toto')
self.assertTrue(msg['apply_to']['retract'])
suite = unittest.TestLoader().loadTestsFromTestCase(TestRetract) suite = unittest.TestLoader().loadTestsFromTestCase(TestRetract)

View File

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

View File

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

View File

@ -33,17 +33,12 @@ CLASSIFIERS = [
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', '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 :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')] packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
def check_include(library_name, header): def check_include(library_name, header):
command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name] command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name]
try: try:
@ -64,7 +59,6 @@ def check_include(library_name, header):
print('%s headers not found.' % library_name) print('%s headers not found.' % library_name)
return False return False
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h') HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h') HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
@ -93,7 +87,7 @@ setup(
packages=packages, packages=packages,
ext_modules=ext_modules, ext_modules=ext_modules,
install_requires=[ install_requires=[
'aiodns >= 1.0; sys_platform=="linux" or sys_platform=="darwin"', 'aiodns>=1.0',
'pyasn1', 'pyasn1',
'pyasn1_modules', 'pyasn1_modules',
'typing_extensions; python_version < "3.8.0"', 'typing_extensions; python_version < "3.8.0"',

View File

@ -27,9 +27,3 @@ from slixmpp.clientxmpp import ClientXMPP
from slixmpp.componentxmpp import ComponentXMPP from slixmpp.componentxmpp import ComponentXMPP
from slixmpp.version import __version__, __version_info__ 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

@ -315,12 +315,13 @@ class BaseXMPP(XMLStream):
pres['lang'] = self.default_lang pres['lang'] = self.default_lang
return pres return pres
def make_iq(self, id: Optional[str] = None, ifrom: OptJidStr = None, def make_iq(self, id: str = "0", ifrom: OptJidStr = None,
ito: OptJidStr = None, itype: Optional[IqTypes] = None, ito: OptJidStr = None, itype: Optional[IqTypes] = None,
iquery: Optional[str] = None) -> stanza.Iq: iquery: Optional[str] = None) -> stanza.Iq:
"""Create a new :class:`~.Iq` stanza with a given Id and from JID. """Create a new :class:`~.Iq` stanza with a given Id and from JID.
:param id: An ideally unique ID value for this stanza thread. :param id: An ideally unique ID value for this stanza thread.
Defaults to 0.
:param ifrom: The from :class:`~.JID` :param ifrom: The from :class:`~.JID`
to use for this stanza. to use for this stanza.
:param ito: The destination :class:`~.JID` :param ito: The destination :class:`~.JID`
@ -331,8 +332,7 @@ class BaseXMPP(XMLStream):
:param iquery: Optional namespace for adding a query element. :param iquery: Optional namespace for adding a query element.
""" """
iq = self.Iq() iq = self.Iq()
if id is not None: iq['id'] = str(id)
iq['id'] = str(id)
iq['to'] = ito iq['to'] = ito
iq['from'] = ifrom iq['from'] = ifrom
iq['type'] = itype iq['type'] = itype

View File

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

View File

@ -9,7 +9,6 @@
import logging import logging
import hashlib import hashlib
from asyncio import Future
from typing import Optional from typing import Optional
from slixmpp import Message, Iq, Presence from slixmpp import Message, Iq, Presence
@ -98,7 +97,7 @@ class ComponentXMPP(BaseXMPP):
def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None, def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None, force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> Future: disable_starttls: Optional[bool] = None) -> None:
"""Connect to the server. """Connect to the server.
@ -119,7 +118,7 @@ class ComponentXMPP(BaseXMPP):
self.server_name = self.boundjid.host self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port) log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=self.server_host, port=self.server_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): def incoming_filter(self, xml):
""" """

View File

@ -135,7 +135,6 @@ _DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = {
"not-allowed": "cancel", "not-allowed": "cancel",
"not-authorized": "auth", "not-authorized": "auth",
"payment-required": "auth", "payment-required": "auth",
"policy-violation": "modify",
"recipient-unavailable": "wait", "recipient-unavailable": "wait",
"redirect": "modify", "redirect": "modify",
"registration-required": "auth", "registration-required": "auth",

View File

@ -112,19 +112,15 @@ PLUGINS = [
'xep_0421', # Anonymous unique occupant identifiers for MUCs 'xep_0421', # Anonymous unique occupant identifiers for MUCs
'xep_0422', # Message Fastening 'xep_0422', # Message Fastening
'xep_0424', # Message Retraction 'xep_0424', # Message Retraction
'xep_0425', # Moderated Message Retraction 'xep_0425', # Message Moderation
'xep_0428', # Message Fallback 'xep_0428', # Message Fallback
'xep_0437', # Room Activity Indicators 'xep_0437', # Room Activity Indicators
'xep_0439', # Quick Response 'xep_0439', # Quick Response
'xep_0441', # Message Archive Management Preferences 'xep_0441', # Message Archive Management Preferences
'xep_0444', # Message Reactions 'xep_0444', # Message Reactions
'xep_0446', # File metadata element
'xep_0447', # Stateless file sharing 'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies 'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning 'xep_0469', # Bookmarks Pinning
'xep_0482', # Call Invites
'xep_0490', # Message Displayed Synchronization
'xep_0492', # Chat Notification Settings
# Meant to be imported by plugins # Meant to be imported by plugins
] ]

View File

@ -6,18 +6,14 @@
# Part of Slixmpp: The Slick XMPP Library # Part of Slixmpp: The Slick XMPP Library
# :copyright: (c) 2012 Nathanael C. Fritz # :copyright: (c) 2012 Nathanael C. Fritz
# :license: MIT, see LICENSE for more details # :license: MIT, see LICENSE for more details
from __future__ import annotations
import sys import sys
import copy import copy
import logging import logging
import threading import threading
from typing import Any, Dict, Set, ClassVar, Union, TYPE_CHECKING from typing import Any, Dict, Set, ClassVar
if TYPE_CHECKING:
from slixmpp.clientxmpp import ClientXMPP
from slixmpp.componentxmpp import ComponentXMPP
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -276,7 +272,7 @@ class BasePlugin(object):
#: `plugin.config['foo']`. #: `plugin.config['foo']`.
default_config: ClassVar[Dict[str, Any]] = {} default_config: ClassVar[Dict[str, Any]] = {}
def __init__(self, xmpp: Union[ClientXMPP,ComponentXMPP], config=None): def __init__(self, xmpp, config=None):
self.xmpp = xmpp self.xmpp = xmpp
if self.xmpp: if self.xmpp:
self.api = self.xmpp.api.wrap(self.name) self.api = self.xmpp.api.wrap(self.name)

View File

@ -1,9 +1,8 @@
# Slixmpp: The Slick XMPP Library # Slixmpp: The Slick XMPP Library
# Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp. # This file is part of Slixmpp.
# See the file LICENSE for copying permission. # See the file LICENSE for copying permission.
import logging
from slixmpp.xmlstream import ElementBase, ET from slixmpp.xmlstream import ElementBase, ET
@ -79,14 +78,7 @@ class FormField(ElementBase):
reqXML = self.xml.find('{%s}required' % self.namespace) reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None return reqXML is not None
def get_value(self, convert=True, convert_list=False): def get_value(self, convert=True):
"""
Gets the value for this field
:param convert: Convert truthy values to boolean
:param convert_list: Convert text-multi fields to a string with
\n as separator for values
"""
valsXML = self.xml.findall('{%s}value' % self.namespace) valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0: if len(valsXML) == 0:
return None return None
@ -100,7 +92,7 @@ class FormField(ElementBase):
if valXML.text is None: if valXML.text is None:
valXML.text = '' valXML.text = ''
values.append(valXML.text) values.append(valXML.text)
if self._type == 'text-multi' and convert_list: if self._type == 'text-multi' and convert:
values = "\n".join(values) values = "\n".join(values)
return values return values
else: else:
@ -135,17 +127,6 @@ class FormField(ElementBase):
del self['value'] del self['value']
valXMLName = '{%s}value' % self.namespace valXMLName = '{%s}value' % self.namespace
if not self._type:
if isinstance(value, bool):
log.debug("Passed a 'boolean' as value of an untyped field, assuming it is a 'boolean'")
self._type = "boolean"
elif isinstance(value, str):
log.debug("Passed a 'str' as value of an untyped field, assuming it is a 'text-single'")
self._type = "text-single"
elif isinstance(value, (list, tuple)):
log.debug("Passed a %s as value of an untyped field, assuming it is a 'text-multi'")
self._type = "text-multi"
if self._type == 'boolean': if self._type == 'boolean':
if value in self.true_values: if value in self.true_values:
valXML = ET.Element(valXMLName) valXML = ET.Element(valXMLName)
@ -199,6 +180,3 @@ FormField.setOptions = FormField.set_options
FormField.setRequired = FormField.set_required FormField.setRequired = FormField.set_required
FormField.setTrue = FormField.set_true FormField.setTrue = FormField.set_true
FormField.setValue = FormField.set_value FormField.setValue = FormField.set_value
log = logging.getLogger(__name__)

View File

@ -162,7 +162,7 @@ class XEP_0009(BasePlugin):
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1: if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
return return
# Reply with error by default # Reply with error by default
error = self.xmpp.plugin['xep_0009']._item_not_found(iq) error = self.client.plugin['xep_0009']._item_not_found(iq)
error.send() error.send()
def _on_jabber_rpc_method_response(self, iq, forwarded=False): def _on_jabber_rpc_method_response(self, iq, forwarded=False):
@ -175,7 +175,7 @@ class XEP_0009(BasePlugin):
""" """
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1: if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
return return
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq) error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error.send() error.send()
def _on_jabber_rpc_method_fault(self, iq, forwarded=False): def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
@ -188,7 +188,7 @@ class XEP_0009(BasePlugin):
""" """
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1: if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
return return
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq) error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error.send() error.send()
def _on_jabber_rpc_error(self, iq, forwarded=False): def _on_jabber_rpc_error(self, iq, forwarded=False):
@ -201,7 +201,7 @@ class XEP_0009(BasePlugin):
""" """
if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1: if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
return return
error = self.xmpp.plugin['xep_0009']._recipient_unvailable(iq) error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
error.send() error.send()
def _send_fault(self, iq, fault_xml): # def _send_fault(self, iq, fault_xml): #

View File

@ -57,9 +57,6 @@ class XEP_0030(BasePlugin):
Given Given A single node Given Given A single node
====== ======= ============================ ====== ======= ============================
Adding information for a given node without specifying the JID will
use the bound JID and therefore must be done after the bind happens.
Stream Handlers: Stream Handlers:
:: ::

View File

@ -9,7 +9,6 @@ from typing import (
Set, Set,
Tuple, Tuple,
Union, Union,
Dict,
) )
from slixmpp.xmlstream import ElementBase, ET from slixmpp.xmlstream import ElementBase, ET
@ -145,25 +144,6 @@ class DiscoInfo(ElementBase):
return True return True
return False return False
def dict_identities(self, lang: Optional[str] = None) -> Set[Dict[str, str]]:
"""
Return the set of all identities, each one as a dict with
category, type, xml_lang, and name keys.
:param lang: If there is a need to filter identities by lang.
"""
ids = self.get_identities(lang=lang, dedupe=True)
dict_ids = set()
for identity in ids:
dict_ids.add({
'category': identity[0],
'type': identity[1],
'xml_lang': identity[2],
'name': identity[3],
})
return dict_ids
def get_identities(self, lang: Optional[str] = None, dedupe: bool = True def get_identities(self, lang: Optional[str] = None, dedupe: bool = True
) -> Iterable[IdentityType]: ) -> Iterable[IdentityType]:
""" """
@ -185,11 +165,11 @@ class DiscoInfo(ElementBase):
identities = [] identities = []
for id_xml in self.xml.findall('{%s}identity' % self.namespace): for id_xml in self.xml.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) 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: if lang is None or xml_lang == lang:
id = (category, type_, xml_lang, name) 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))
if isinstance(identities, set): if isinstance(identities, set):
identities.add(id) identities.add(id)
else: else:
@ -273,12 +253,10 @@ class DiscoInfo(ElementBase):
else: else:
features = [] features = []
for feature_xml in self.xml.findall('{%s}feature' % self.namespace): for feature_xml in self.xml.findall('{%s}feature' % self.namespace):
feature = feature_xml.attrib.get('var', None) if isinstance(features, set):
if feature: features.add(feature_xml.attrib['var'])
if isinstance(features, set): else:
features.add(feature) features.append(feature_xml.attrib['var'])
else:
features.append(feature)
return features return features
def set_features(self, features: Iterable[str]): def set_features(self, features: Iterable[str]):

View File

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

View File

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

View File

@ -326,10 +326,7 @@ class XEP_0050(BasePlugin):
iq['command']['actions'] = actions iq['command']['actions'] = actions
iq['command']['status'] = 'executing' iq['command']['status'] = 'executing'
else: else:
actions = ['complete'] iq['command']['actions'] = ['complete']
if session['allow_prev']:
actions.append('prev')
iq['command']['actions'] = actions
iq['command']['status'] = 'executing' iq['command']['status'] = 'executing'
iq['command']['notes'] = session['notes'] iq['command']['notes'] = session['notes']

View File

@ -464,10 +464,10 @@ class XEP_0060(BasePlugin):
""" """
Retrieve the ItemIDs hosted by a given node, using disco. Retrieve the ItemIDs hosted by a given node, using disco.
""" """
return self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom, self.xmpp['xep_0030'].get_items(jid, node, ifrom=ifrom,
callback=callback, timeout=timeout, callback=callback, timeout=timeout,
iterator=iterator, iterator=iterator,
timeout_callback=timeout_callback) timeout_callback=timeout_callback)
def modify_affiliations(self, jid, node, affiliations=None, ifrom=None, def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
timeout_callback=None, callback=None, timeout_callback=None, callback=None,

View File

@ -137,14 +137,7 @@ class XEP_0199(BasePlugin):
async def _keepalive(self, event=None): async def _keepalive(self, event=None):
log.debug("Keepalive ping...") log.debug("Keepalive ping...")
try: try:
ifrom = None rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
if self.xmpp.is_component:
ifrom = self.xmpp.boundjid
rtt = await self.ping(
self.xmpp.boundjid.host,
timeout=self.timeout,
ifrom=ifrom
)
except IqTimeout: except IqTimeout:
log.debug("Did not receive ping back in time. " + \ log.debug("Did not receive ping back in time. " + \
"Requesting Reconnect.") "Requesting Reconnect.")

View File

@ -20,18 +20,6 @@ class XEP_0223(BasePlugin):
""" """
XEP-0223: Persistent Storage of Private Data via PubSub 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' name = 'xep_0223'
@ -40,7 +28,6 @@ class XEP_0223(BasePlugin):
profile = {'pubsub#persist_items': True, profile = {'pubsub#persist_items': True,
'pubsub#access_model': 'whitelist'} 'pubsub#access_model': 'whitelist'}
node_profiles = dict[str, dict[str, str]]()
def configure(self, node: str, **iqkwargs) -> Future: def configure(self, node: str, **iqkwargs) -> Future:
""" """
@ -48,12 +35,8 @@ class XEP_0223(BasePlugin):
:param node: Node to set the configuration at. :param node: Node to set the configuration at.
""" """
config = self.xmpp['xep_0004'].stanza.Form() config = self.xmpp['xep_0004'].Form()
config['type'] = 'submit' config['type'] = 'submit'
config.add_field(
var='FORM_TYPE',
ftype='hidden',
value='http://jabber.org/protocol/pubsub#node_config')
for field, value in self.profile.items(): for field, value in self.profile.items():
config.add_field(var=field, value=value) config.add_field(var=field, value=value)
@ -87,8 +70,7 @@ class XEP_0223(BasePlugin):
value='http://jabber.org/protocol/pubsub#publish-options') value='http://jabber.org/protocol/pubsub#publish-options')
fields = options['fields'] fields = options['fields']
profile = self.profile | self.node_profiles.get(node, {}) for field, value in self.profile.items():
for field, value in profile.items():
if field not in fields: if field not in fields:
options.add_field(var=field) options.add_field(var=field)
options.get_fields()[field]['value'] = value options.get_fields()[field]['value'] = value

View File

@ -10,7 +10,6 @@ from asyncio import Future
from typing import Optional from typing import Optional
from slixmpp import JID from slixmpp import JID
from slixmpp.exceptions import XMPPError
from slixmpp.stanza import Iq, Message, Presence from slixmpp.stanza import Iq, Message, Presence
from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.handler import CoroutineCallback
from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.matcher import StanzaPath
@ -140,13 +139,6 @@ class XEP_0231(BasePlugin):
self.xmpp.event('bob', iq) self.xmpp.event('bob', iq)
elif iq['type'] == 'get': elif iq['type'] == 'get':
data = await self.api['get_bob'](iq['to'], None, iq['from'], args=cid) 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): if isinstance(data, Iq):
data['id'] = iq['id'] data['id'] = iq['id']
data.send() data.send()

View File

@ -1,6 +1,5 @@
from slixmpp.plugins.base import register_plugin from slixmpp.plugins.base import register_plugin
from . import stanza, vcard4 from . import stanza, vcard4
from .vcard4 import XEP_0292
register_plugin(vcard4.XEP_0292) register_plugin(vcard4.XEP_0292)

View File

@ -1,12 +1,12 @@
# Slixmpp: The Slick XMPP Library # Slixmpp: The Slick XMPP Library
# Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp. # This file is part of Slixmpp.
# See the file LICENSE for copying permissio # See the file LICENSE for copying permissio
import logging import logging
from typing import Optional
import slixmpp
from slixmpp.stanza import Message from slixmpp.stanza import Message
from slixmpp.jid import JID
from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream import register_stanza_plugin
@ -45,59 +45,5 @@ class XEP_0308(BasePlugin):
def session_bind(self, jid): def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace) self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
def is_correction(self, msg: Message): def _handle_correction(self, msg):
return msg.xml.find('{%s}replace' % Replace.namespace) is not None
def _handle_correction(self, msg: Message):
self.xmpp.event('message_correction', msg) self.xmpp.event('message_correction', msg)
def build_correction(self, id_to_replace: str, mto: JID,
mfrom: Optional[JID] = None, mtype: str = 'chat',
mbody: str = '') -> Message:
"""
Build a corrected message.
:param id_to_replace: The id of the original message.
:param mto: Recipient of the message, must be the same as the original
message.
:param mfrom: Sender of the message, must be the same as the original
message.
:param mtype: Type of the message, must be the send as the original
message.
:param mbody: The corrected message body.
"""
msg = self.xmpp.make_message(
mto=mto,
mfrom=mfrom,
mbody=mbody,
mtype=mtype
)
msg['replace']['id'] = id_to_replace
return msg
def correct_message(self, msg: Message, body: str) -> Message:
"""
Send a correction to an existing message.
:param msg: The message that must be replaced.
:param body: The body to set in the correcting message.
:returns: The message that was sent.
"""
to_replace = msg['id']
mto = msg['to']
mfrom = msg['from']
mtype = msg['type']
if not to_replace:
raise ValueError('No available ID for replacing the message')
if not mto:
raise ValueError('No available recipient JID')
new = self.build_correction(
id_to_replace=to_replace,
mto=mto,
mfrom=mfrom,
mtype=mtype,
mbody=body,
)
new.send()
return new

View File

@ -138,10 +138,10 @@ class XEP_0356(BasePlugin):
Raises ValueError if the server did not advertise the corresponding privileges Raises ValueError if the server did not advertise the corresponding privileges
Here is an example of a roster_items value: :param jid: user we want to add or modify roster items
:param roster_items: a dict containing the roster items' JIDs as keys and
.. code-block:: json nested dicts containing names, subscriptions and groups.
Example:
{ {
"friend1@example.com": { "friend1@example.com": {
"name": "Friend 1", "name": "Friend 1",
@ -152,13 +152,8 @@ class XEP_0356(BasePlugin):
"name": "Friend 2", "name": "Friend 2",
"subscription": "from", "subscription": "from",
"groups": ["group3"], "groups": ["group3"],
}, },
} }
:param jid: user we want to add or modify roster items
:param roster_items: a dict containing the roster items' JIDs as keys and
nested dicts containing names, subscriptions and groups.
""" """
if isinstance(jid, str): if isinstance(jid, str):
jid = JID(jid) jid = JID(jid)

View File

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

View File

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

View File

@ -8,27 +8,28 @@ from slixmpp.xmlstream import (
ElementBase, ElementBase,
register_stanza_plugin, register_stanza_plugin,
) )
from slixmpp.plugins.xep_0422.stanza import ApplyTo
from slixmpp.plugins.xep_0359 import OriginID
NS = 'urn:xmpp:message-retract:1' NS = 'urn:xmpp:message-retract:0'
class Retract(ElementBase): class Retract(ElementBase):
namespace = NS namespace = NS
name = 'retract' name = 'retract'
plugin_attrib = 'retract' plugin_attrib = 'retract'
interfaces = {'reason', 'id'}
sub_interfaces = {'reason'}
class Retracted(ElementBase): class Retracted(ElementBase):
namespace = NS namespace = NS
name = 'retracted' name = 'retracted'
plugin_attrib = 'retracted' plugin_attrib = 'retracted'
interfaces = {'stamp', 'id', 'reason'} interfaces = {'stamp'}
sub_interfaces = {'reason'}
def register_plugins(): def register_plugins():
register_stanza_plugin(Message, Retract) register_stanza_plugin(ApplyTo, Retract)
register_stanza_plugin(Message, Retracted) 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): class XEP_0425(BasePlugin):
'''XEP-0425: Moderated Message Retraction''' '''XEP-0425: Message Moderation'''
name = 'xep_0425' name = 'xep_0425'
description = 'XEP-0425: Moderated Message Retraction' description = 'XEP-0425: Message Moderation'
dependencies = {'xep_0424', 'xep_0421'} dependencies = {'xep_0424', 'xep_0421'}
stanza = stanza stanza = stanza
namespace = stanza.NS namespace = stanza.NS
@ -25,7 +25,7 @@ class XEP_0425(BasePlugin):
stanza.register_plugins() stanza.register_plugins()
self.xmpp.register_handler(Callback( self.xmpp.register_handler(Callback(
'Moderated Message', 'Moderated Message',
StanzaPath('message/retract/moderated'), StanzaPath('message/apply_to/moderated/retract'),
self._handle_moderated, self._handle_moderated,
)) ))
@ -42,7 +42,7 @@ class XEP_0425(BasePlugin):
async def moderate(self, room: JID, id: str, reason: str = '', *, async def moderate(self, room: JID, id: str, reason: str = '', *,
ifrom: Optional[JID] = None, **iqkwargs): ifrom: Optional[JID] = None, **iqkwargs):
iq = self.xmpp.make_iq_set(ito=room.bare, ifrom=ifrom) iq = self.xmpp.make_iq_set(ito=room.bare, ifrom=ifrom)
iq['moderate']['id'] = id iq['apply_to']['id'] = id
iq['moderate']['reason'] = reason iq['apply_to']['moderate']['reason'] = reason
iq['moderate'].enable('retract') iq['apply_to']['moderate'].enable('retract')
await iq.send(**iqkwargs) await iq.send(**iqkwargs)

View File

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

View File

@ -18,7 +18,3 @@ class XEP_0446(BasePlugin):
name = "xep_0446" name = "xep_0446"
description = "XEP-0446: File metadata element" description = "XEP-0446: File metadata element"
stanza = stanza stanza = stanza
dependencies = {'xep_0300', 'xep_0264'}
def plugin_init(self):
stanza.register_plugins()

View File

@ -1,10 +1,7 @@
from datetime import datetime from datetime import datetime
from typing import Optional
from slixmpp.plugins.xep_0082 import format_datetime, parse from slixmpp.plugins.xep_0082 import format_datetime, parse
from slixmpp.plugins.xep_0300 import Hash from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0264.stanza import Thumbnail
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = "urn:xmpp:file:metadata:0" NS = "urn:xmpp:file:metadata:0"
@ -13,42 +10,15 @@ class File(ElementBase):
name = "file" name = "file"
namespace = NS namespace = NS
plugin_attrib = "file" plugin_attrib = "file"
interfaces = sub_interfaces = { interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"}
"media-type",
"name",
"date",
"size",
"desc",
"width",
"height",
"length"
}
def set_width(self, width: int):
self.__set_if_positive("width", width)
def get_width(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("width"))
def set_height(self, height: int):
self.__set_if_positive("height", height)
def get_height(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("height"))
def set_length(self, length: int):
self.__set_if_positive("length", length)
def get_length(self) -> Optional[int]:
return _positive_int_or_none(self._get_sub_text("length"))
def set_size(self, size: int): def set_size(self, size: int):
self.__set_if_positive("size", size) self._set_sub_text("size", str(size))
def get_size(self) -> Optional[int]: def get_size(self):
return _positive_int_or_none(self._get_sub_text("size")) return _int_or_none(self._get_sub_text("size"))
def get_date(self) -> Optional[datetime]: def get_date(self):
try: try:
return parse(self._get_sub_text("date")) return parse(self._get_sub_text("date"))
except ValueError: except ValueError:
@ -60,18 +30,9 @@ class File(ElementBase):
except ValueError: except ValueError:
pass pass
def __set_if_positive(self, key: str, value: int):
if value <= 0:
raise ValueError(f"Invalid value for element {key}: {value}")
self._set_sub_text(key, str(value))
def _int_or_none(v):
def _positive_int_or_none(v: str) -> Optional[int]:
try: try:
return int(v) return int(v)
except ValueError: except ValueError:
return None return None
def register_plugins():
register_stanza_plugin(File, Hash)
register_stanza_plugin(File, Thumbnail)

View File

@ -1,5 +1,3 @@
from typing import Optional
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.types import JidStr from slixmpp.types import JidStr
from slixmpp.xmlstream import StanzaBase from slixmpp.xmlstream import StanzaBase
@ -38,35 +36,13 @@ class XEP_0461(BasePlugin):
def _handle_reply_to_message(self, msg: StanzaBase): def _handle_reply_to_message(self, msg: StanzaBase):
self.xmpp.event("message_reply", msg) self.xmpp.event("message_reply", msg)
def make_reply(self, reply_to: JidStr, reply_id: str, def send_reply(self, reply_to: JidStr, reply_id: str, **msg_kwargs):
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_to: Full JID of the quoted author
:param reply_id: ID of the message to reply to :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 = self.xmpp.make_message(**msg_kwargs)
msg["reply"]["to"] = reply_to msg["reply"]["to"] = reply_to
msg["reply"]["id"] = reply_id 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.make_reply(reply_to, reply_id, fallback, quoted_nick, **msg_kwargs)
msg.send() msg.send()

View File

@ -14,12 +14,11 @@ class Reply(ElementBase):
interfaces = {"id", "to"} interfaces = {"id", "to"}
def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None): def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None):
r""" """
Add plain text fallback for clients not implementing XEP-0461. Add plain text fallback for clients not implementing XEP-0461.
``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will ``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will
prepend ``> Bob:\n> Some text\n`` to the body of the message, and set the prepend "> Bob:\n> Some text\n" to the body of the message, and set the
fallback_body attributes accordingly, so that clients implementing fallback_body attributes accordingly, so that clients implementing
XEP-0461 can hide the fallback text. XEP-0461 can hide the fallback text.
@ -31,11 +30,11 @@ class Reply(ElementBase):
if nickname: if nickname:
quoted = "> " + nickname + ":\n" + quoted quoted = "> " + nickname + ":\n" + quoted
msg["body"] = quoted + msg["body"] msg["body"] = quoted + msg["body"]
fallback_elem = Fallback() fallback = Fallback()
fallback_elem["for"] = NS fallback["for"] = NS
fallback_elem["body"]["start"] = 0 fallback["body"]["start"] = 0
fallback_elem["body"]["end"] = len(quoted) fallback["body"]["end"] = len(quoted)
msg.append(fallback_elem) msg.append(fallback)
def get_fallback_body(self) -> str: def get_fallback_body(self) -> str:
msg = self.parent() msg = self.parent()
@ -51,23 +50,6 @@ class Reply(ElementBase):
return body[start:end] return body[start:end]
else: else:
return "" return ""
def strip_fallback_content(self) -> str:
msg = self.parent()
for fallback in msg["fallbacks"]:
if fallback["for"] == NS:
break
else:
return msg["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 register_plugins(): def register_plugins():

View File

@ -1,11 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0482 import stanza
from slixmpp.plugins.xep_0482.call_invites import XEP_0482
register_plugin(XEP_0482)

View File

@ -1,55 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permissio
import logging
from typing import Optional
from slixmpp.stanza import Message
from slixmpp.jid import JID
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0482 import stanza
log = logging.getLogger(__name__)
class XEP_0482(BasePlugin):
"""
XEP-0482: Call Invites
This plugin defines the stanza elements for Call Invites, as well as new
events:
- `call-invite`
- `call-reject`
- `call-retract`
- `call-leave`
- `call-left`
"""
name = 'xep_0482'
description = 'XEP-0482: Call Invites'
dependencies = set()
stanza = stanza
def plugin_init(self):
stanza.register_plugins()
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
self.xmpp.register_handler(
Callback(f'Call {event}',
StanzaPath(f'message/call-{event}'),
self._handle_event))
def _handle_event(self, message):
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
if message.get_plugin(f'call-{event}', check=True):
self.xmpp.event(f'call-{event}')
def plugin_end(self):
for event in ('invite', 'reject', 'retract', 'leave', 'left'):
self.xmpp.remove_handler(f'Call {event}')

View File

@ -1,102 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 Mathieu Pasquet
# This file is part of Slixmpp.
# See the file LICENSE for copying permission
from typing import Tuple, List, Optional
from slixmpp import Message
from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
NS = 'urn:xmpp:call-invites:0'
class Jingle(ElementBase):
name = 'jingle'
namespace = NS
plugin_attrib = 'jingle'
plugin_multi_attrib = 'jingles'
interfaces = {'sid', 'jid'}
def set_jid(self, value: JID) -> None:
if not isinstance(value, JID):
try:
value = JID(value)
except ValueError:
raise ValueError(f'"jid" must be a valid JID object')
self.xml.attrib['jid'] = value.full
def get_jid(self) -> Optional[JID]:
try:
return JID(self.xml.attrib.get('jid', ''))
except ValueError:
return None
class External(ElementBase):
name = 'external'
namespace = NS
plugin_attrib = 'external'
plugin_multi_attrib = 'externals'
interfaces = {'uri'}
class Invite(ElementBase):
name = 'invite'
namespace = NS
plugin_attrib = 'call-invite'
interfaces = {'video'}
def get_methods(self) -> Tuple[List[Jingle], List[External]]:
return (self['jingles'], self['externals'])
def set_video(self, value: bool) -> None:
if not isinstance(value, bool):
raise ValueError(f'Invalid value for the video attribute: {value}')
self.xml.attrib['video'] = str(value).lower()
def get_video(self) -> bool:
vid = self.xml.attrib.get('video', 'false').lower()
return vid == 'true'
class Retract(ElementBase):
name = 'retract'
namespace = NS
plugin_attrib = 'call-retract'
interfaces = {'id'}
class Accept(ElementBase):
name = 'accept'
namespace = NS
plugin_attrib = 'call-accept'
interfaces = {'id'}
class Reject(ElementBase):
name = 'reject'
namespace = NS
plugin_attrib = 'call-reject'
interfaces = {'id'}
class Left(ElementBase):
name = 'left'
namespace = NS
plugin_attrib = 'call-left'
interfaces = {'id'}
def register_plugins() -> None:
register_stanza_plugin(Message, Invite)
register_stanza_plugin(Message, Retract)
register_stanza_plugin(Message, Accept)
register_stanza_plugin(Message, Reject)
register_stanza_plugin(Message, Left)
register_stanza_plugin(Invite, Jingle, iterable=True)
register_stanza_plugin(Invite, External, iterable=True)
register_stanza_plugin(Accept, Jingle)
register_stanza_plugin(Accept, External)

View File

@ -1,8 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,13 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins.base import register_plugin
from . import stanza
from .notify import XEP_0492
register_plugin(XEP_0492)
__all__ = ["stanza", "XEP_0492"]

View File

@ -1,21 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from slixmpp.plugins import BasePlugin
from . import stanza
class XEP_0492(BasePlugin):
"""
XEP-0492: Chat notification settings
"""
name = "xep_0492"
description = "XEP-0492: Chat notification settings"
dependencies = {"xep_0402"}
stanza = stanza
def plugin_init(self):
stanza.register_plugin()

View File

@ -1,106 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
from typing import Literal, Optional, cast
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0402.stanza import Extensions
from slixmpp.types import ClientTypes
from slixmpp.xmlstream import ElementBase
NS = "urn:xmpp:notification-settings:0"
WhenLiteral = Literal["never", "always", "on-mention"]
class Notify(ElementBase):
"""
Chat notification settings element
To enable it on a Conference element, use configure() like this:
.. code-block::python
# C being a Conference element
C['extensions']["notify"].configure("always", client_type="pc")
Which will add the <notify> element to the <extensions> element.
"""
namespace = NS
name = "notify"
plugin_attrib = "notify"
interfaces = {"notify"}
def configure(self, when: WhenLiteral, client_type: Optional[ClientTypes] = None) -> None:
"""
Configure the chat notification settings for this bookmark.
This method ensures that there are no conflicting settings, e.g.,
both a <never /> and a <always /> element.
"""
cls = _CLASS_MAP[when]
element = cls()
if client_type is not None:
element["client-type"] = client_type
match = client_type if client_type is not None else ""
for child in self:
if isinstance(child, _Base) and child["client-type"] == match:
self.xml.remove(child.xml)
self.append(element)
def get_config(
self, client_type: Optional[ClientTypes] = None
) -> Optional[WhenLiteral]:
"""
Get the chat notification settings for this bookmark.
:param client_type: Optionally, get the notification for a specific client type.
If unset, returns the global notification setting.
:return: The chat notification setting as a string, or None if unset.
"""
match = client_type if client_type is not None else ""
for child in self:
if isinstance(child, _Base) and child["client-type"] == match:
return cast(WhenLiteral, child.name)
return None
class _Base(ElementBase):
namespace = NS
interfaces = {"client-type"}
class Never(_Base):
name = "never"
class Always(_Base):
name = "always"
class OnMention(_Base):
name = "on-mention"
class Advanced(ElementBase):
namespace = NS
name = plugin_attrib = "advanced"
_CLASS_MAP = {
"never": Never,
"always": Always,
"on-mention": OnMention,
}
def register_plugin():
register_stanza_plugin(Extensions, Notify)
register_stanza_plugin(Notify, Advanced)

View File

@ -103,7 +103,6 @@ from slixmpp.plugins.xep_0437 import XEP_0437
from slixmpp.plugins.xep_0439 import XEP_0439 from slixmpp.plugins.xep_0439 import XEP_0439
from slixmpp.plugins.xep_0444 import XEP_0444 from slixmpp.plugins.xep_0444 import XEP_0444
from slixmpp.plugins.xep_0461 import XEP_0461 from slixmpp.plugins.xep_0461 import XEP_0461
from slixmpp.plugins.xep_0490 import XEP_0490
class PluginsDict(TypedDict): class PluginsDict(TypedDict):
@ -200,4 +199,3 @@ class PluginsDict(TypedDict):
xep_0439: XEP_0439 xep_0439: XEP_0439
xep_0444: XEP_0444 xep_0444: XEP_0444
xep_0461: XEP_0461 xep_0461: XEP_0461
xep_0490: XEP_0490

View File

@ -53,20 +53,17 @@ MucAffiliation = Literal[
'outcast', 'member', 'admin', 'owner', 'none' 'outcast', 'member', 'admin', 'owner', 'none'
] ]
OptJid = Optional[JID]
JidStr = Union[str, JID]
OptJidStr = Optional[Union[str, JID]]
class PresenceArgs(TypedDict, total=False): class PresenceArgs(TypedDict, total=False):
pfrom: JidStr pfrom: JID
pto: JidStr pto: JID
pshow: PresenceShows pshow: PresenceShows
ptype: PresenceTypes ptype: PresenceTypes
pstatus: str pstatus: str
class MucRoomItem(TypedDict, total=False): class MucRoomItem(TypedDict, total=False):
jid: str jid: JID
role: MucRole role: MucRole
affiliation: MucAffiliation affiliation: MucAffiliation
show: Optional[PresenceShows] show: Optional[PresenceShows]
@ -78,6 +75,10 @@ MucRoomItemKeys = Literal[
'jid', 'role', 'affiliation', 'show', 'status', 'alt_nick', 'jid', 'role', 'affiliation', 'show', 'status', 'alt_nick',
] ]
OptJid = Optional[JID]
JidStr = Union[str, JID]
OptJidStr = Optional[Union[str, JID]]
MAMDefault = Literal['always', 'never', 'roster'] MAMDefault = Literal['always', 'never', 'roster']
FilterString = Literal['in', 'out', 'out_sync'] FilterString = Literal['in', 'out', 'out_sync']
@ -97,7 +98,6 @@ ErrorConditions = Literal[
"not-allowed", "not-allowed",
"not-authorized", "not-authorized",
"payment-required", "payment-required",
"policy-violation",
"recipient-unavailable", "recipient-unavailable",
"redirect", "redirect",
"registration-required", "registration-required",
@ -110,21 +110,8 @@ ErrorConditions = Literal[
"unexpected-request", "unexpected-request",
] ]
# https://xmpp.org/registrar/disco-categories.html#client
ClientTypes = Literal[
"bot",
"console",
"game",
"handheld",
"pc",
"phone",
"sms",
"tablet",
"web",
]
__all__ = [ __all__ = [
'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault', 'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault',
'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole', 'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes', 'ClientTypes' 'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes'
] ]

View File

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

View File

@ -732,9 +732,6 @@ class ElementBase(object):
return plugin[full_attrib] return plugin[full_attrib]
return plugin return plugin
else: 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 '' return ''
def __setitem__(self, attrib: str, value: Any) -> Any: def __setitem__(self, attrib: str, value: Any) -> Any:
@ -1233,7 +1230,7 @@ class ElementBase(object):
if type(item) == XML_TYPE: if type(item) == XML_TYPE:
return self.appendxml(item) return self.appendxml(item)
else: else:
raise TypeError(f"Cannot append {item!r} to a stanza") raise TypeError
self.xml.append(item.xml) self.xml.append(item.xml)
if item.__class__ == self.plugin_tag_map.get(item.tag_name(), None): if item.__class__ == self.plugin_tag_map.get(item.tag_name(), None):
self.init_plugin(item.plugin_attrib, self.init_plugin(item.plugin_attrib,

View File

@ -281,8 +281,7 @@ class XMLStream(asyncio.BaseProtocol):
__slow_tasks: List[Task] __slow_tasks: List[Task]
__queued_stanzas: List[Tuple[Union[StanzaBase, str], bool]] __queued_stanzas: List[Tuple[Union[StanzaBase, str], bool]]
def __init__(self, host: str = '', port: int = 0, def __init__(self, host: str = '', port: int = 0):
ssl_context: Optional[ssl.SSLContext] = None):
self.transport = None self.transport = None
self.socket = None self.socket = None
self._connect_loop_wait = 0 self._connect_loop_wait = 0
@ -299,12 +298,9 @@ class XMLStream(asyncio.BaseProtocol):
# A dict of {name: handle} # A dict of {name: handle}
self.scheduled_events = {} self.scheduled_events = {}
if ssl_context is None: self.ssl_context = ssl.create_default_context()
self.ssl_context = ssl.create_default_context() self.ssl_context.check_hostname = True
self.ssl_context.check_hostname = True self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
else:
self.ssl_context = ssl_context
self.event_when_connected = "connected" self.event_when_connected = "connected"
@ -375,23 +371,7 @@ class XMLStream(asyncio.BaseProtocol):
@property @property
def loop(self) -> AbstractEventLoop: def loop(self) -> AbstractEventLoop:
if self._loop is None: if self._loop is None:
try: self._loop = asyncio.get_event_loop()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
self._loop = asyncio.get_event_loop()
# We do not know what exception will be raised in the future
# instead of the warning
except Exception:
try:
current = asyncio.get_running_loop()
except RuntimeError:
current = None
if current is not None:
self._loop = current
else:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self._loop = loop
return self._loop return self._loop
@loop.setter @loop.setter
@ -425,10 +405,9 @@ class XMLStream(asyncio.BaseProtocol):
self.disconnected.set_result(True) self.disconnected.set_result(True)
self.disconnected = asyncio.Future() self.disconnected = asyncio.Future()
def connect(self, host: str = '', port: int = 0, def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None,
use_ssl: Optional[bool] = None,
force_starttls: Optional[bool] = None, force_starttls: Optional[bool] = None,
disable_starttls: Optional[bool] = None) -> asyncio.Future: disable_starttls: Optional[bool] = None) -> None:
"""Create a new socket and connect to the server. """Create a new socket and connect to the server.
:param host: The name of the desired server for the connection. :param host: The name of the desired server for the connection.
@ -447,7 +426,6 @@ class XMLStream(asyncio.BaseProtocol):
upgrade to TLS, even if the server provides upgrade to TLS, even if the server provides
it. Use this for example if youre on it. Use this for example if youre on
localhost localhost
:returns: A future on the current connection attempt
""" """
if self._run_out_filters is None or self._run_out_filters.done(): if self._run_out_filters is None or self._run_out_filters.done():
@ -479,14 +457,8 @@ class XMLStream(asyncio.BaseProtocol):
self._connect_routine(), self._connect_routine(),
loop=self.loop, loop=self.loop,
) )
return self._current_connection_attempt
async def _connect_routine(self) -> Optional[asyncio.Future]: async def _connect_routine(self) -> None:
"""
Returns None if the attempt was canceled or if the connection succeeded
(cancelling done manually by the library user, so that should be known)
or the next connection attempt future if a new try has been scheduled.
"""
self.event_when_connected = "connected" self.event_when_connected = "connected"
if self._connect_loop_wait > 0: if self._connect_loop_wait > 0:
@ -511,7 +483,7 @@ class XMLStream(asyncio.BaseProtocol):
ssl_context = None ssl_context = None
if self._current_connection_attempt is None: if self._current_connection_attempt is None:
return None return
try: try:
server_hostname = self.default_domain if self.use_ssl else None server_hostname = self.default_domain if self.use_ssl else None
await self.loop.create_connection(lambda: self, await self.loop.create_connection(lambda: self,
@ -523,12 +495,11 @@ class XMLStream(asyncio.BaseProtocol):
except Socket.gaierror as e: except Socket.gaierror as e:
self.event('connection_failed', self.event('connection_failed',
'No DNS record available for %s' % self.default_domain) 'No DNS record available for %s' % self.default_domain)
return self.reschedule_connection_attempt() self.reschedule_connection_attempt()
except OSError as e: except OSError as e:
log.debug('Connection failed: %s', e) log.debug('Connection failed: %s', e)
self.event("connection_failed", e) self.event("connection_failed", e)
return self.reschedule_connection_attempt() self.reschedule_connection_attempt()
return None
def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None: def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None:
"""Process all the available XMPP events (receiving or sending data on the """Process all the available XMPP events (receiving or sending data on the
@ -664,22 +635,19 @@ class XMLStream(asyncio.BaseProtocol):
self._set_disconnected_future() self._set_disconnected_future()
self.event("disconnected", self.disconnect_reason or exception) self.event("disconnected", self.disconnect_reason or exception)
def reschedule_connection_attempt(self) -> Optional[asyncio.Future]: def reschedule_connection_attempt(self) -> None:
""" """
Increase the exponential back-off and initate another background Increase the exponential back-off and initate another background
_connect_routine call to connect to the server. _connect_routine call to connect to the server.
:returns: A future on the next scheduled connection attempt.
""" """
# abort if there is no ongoing connection attempt # abort if there is no ongoing connection attempt
if self._current_connection_attempt is None: if self._current_connection_attempt is None:
return None return
self._connect_loop_wait = min(300, self._connect_loop_wait * 2 + 1) self._connect_loop_wait = min(300, self._connect_loop_wait * 2 + 1)
self._current_connection_attempt = asyncio.ensure_future( self._current_connection_attempt = asyncio.ensure_future(
self._connect_routine(), self._connect_routine(),
loop=self.loop, loop=self.loop,
) )
return self._current_connection_attempt
def cancel_connection_attempt(self) -> None: def cancel_connection_attempt(self) -> None:
""" """
@ -1382,7 +1350,6 @@ class XMLStream(asyncio.BaseProtocol):
if isinstance(data, (RootStanza, str)) and not passthrough: if isinstance(data, (RootStanza, str)) and not passthrough:
self.__queued_stanzas.append((data, use_filters)) self.__queued_stanzas.append((data, use_filters))
log.debug('NOT SENT: %s %s', type(data), data) log.debug('NOT SENT: %s %s', type(data), data)
self.event('stanza_not_sent', data)
return return
self.waiting_queue.put_nowait((data, use_filters)) self.waiting_queue.put_nowait((data, use_filters))
@ -1446,11 +1413,7 @@ class XMLStream(asyncio.BaseProtocol):
# Convert the raw XML object into a stanza object. If no registered # Convert the raw XML object into a stanza object. If no registered
# stanza type applies, a generic StanzaBase stanza will be used. # stanza type applies, a generic StanzaBase stanza will be used.
try: stanza: Optional[StanzaBase] = self._build_stanza(xml)
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']: for filter in self.__filters['in']:
if stanza is not None: if stanza is not None:
filter = cast(SyncFilter, filter) filter = cast(SyncFilter, filter)

View File

@ -95,21 +95,6 @@ class TestDataForms(SlixTest):
</message> </message>
""") """)
def testMultiLineField(self):
msg = self.Message()
form = msg['form']
form.addField(var='f1',
value='Some text\non several\n\nlines')
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<field var="f1">
<value>Some text\non several\n\nlines</value>
</field>
</x>
</message>
""")
def testSetValues(self): def testSetValues(self):
"""Testing setting form values""" """Testing setting form values"""
@ -132,7 +117,7 @@ class TestDataForms(SlixTest):
<value>b</value> <value>b</value>
</field> </field>
</x> </x>
</message>""", use_values=False) </message>""")
def testSubmitType(self): def testSubmitType(self):
"""Test that setting type to 'submit' clears extra details""" """Test that setting type to 'submit' clears extra details"""

View File

@ -34,7 +34,6 @@ class TestJabberSearch(SlixTest):
ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit" ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
) )
iq["search"]["form"].add_field(var="x-gender", value="male") iq["search"]["form"].add_field(var="x-gender", value="male")
iq["id"] = "0"
self.check( self.check(
iq, iq,
""" """

View File

@ -1,26 +0,0 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0308 import Replace
from slixmpp.xmlstream import register_stanza_plugin
class TestCorrectStanza(SlixTest):
def setUp(self):
register_stanza_plugin(Message, Replace)
def testBuild(self):
"""Test that the element is created correctly."""
msg = Message()
msg['type'] = 'chat'
msg['replace']['id'] = 'toto123'
self.check(msg, """
<message type="chat">
<replace xmlns="urn:xmpp:message-correct:0" id="toto123"/>
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestCorrectStanza)

View File

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

View File

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

View File

@ -1,101 +0,0 @@
import unittest
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0446 import stanza
class TestFileMeta(SlixTest):
def setUp(self):
stanza.register_plugins()
def test_simple(self):
file = stanza.File()
file["desc"] = "a description"
file["name"] = "toto.jpg"
file["media-type"] = "image/jpeg"
file["height"] = 1024
file["width"] = 768
file["size"] = 2048
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>a description</desc>
<name>toto.jpg</name>
<media-type>image/jpeg</media-type>
<height>1024</height>
<width>768</width>
<size>2048</size>
</file>
""",
)
def test_bad_value(self):
file = stanza.File()
file["desc"] = "My great video"
file["name"] = "toto.mp4"
file["media-type"] = "video/3gpp"
file["height"] = 1024
file["width"] = 768
with self.assertRaises(ValueError):
file["length"] = -100
def test_hash_element(self):
file = stanza.File()
file["desc"] = "My great video"
file["name"] = "toto.3gp"
file["media-type"] = "video/3gpp"
file["height"] = 1024
file["width"] = 768
file["length"] = 2000
file["hash"]["algo"] = "sha3-256"
file["hash"]["value"] = "abcdef="
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>My great video</desc>
<name>toto.3gp</name>
<media-type>video/3gpp</media-type>
<height>1024</height>
<width>768</width>
<length>2000</length>
<hash xmlns='urn:xmpp:hashes:2' algo="sha3-256">abcdef=</hash>
</file>
""",
)
def test_thumbnail_element(self):
file = stanza.File()
file["desc"] = "a description"
file["name"] = "toto.jpg"
file["media-type"] = "image/jpeg"
file["height"] = 1024
file["width"] = 768
file["size"] = 2048
file["thumbnail"]["media-type"] = "image/png"
file["thumbnail"]["uri"] = "cid:sha1+deadbeef@bob.xmpp.org"
file["thumbnail"]["width"] = 128
file["thumbnail"]["height"] = 96
self.check(
file,
"""
<file xmlns='urn:xmpp:file:metadata:0'>
<desc>a description</desc>
<name>toto.jpg</name>
<media-type>image/jpeg</media-type>
<height>1024</height>
<width>768</width>
<size>2048</size>
<thumbnail xmlns='urn:xmpp:thumbs:1'
uri='cid:sha1+deadbeef@bob.xmpp.org'
media-type='image/png'
width='128'
height='96'/>
</file>
""",
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestFileMeta)

View File

@ -1,42 +0,0 @@
import unittest
from slixmpp import Message
from slixmpp.jid import JID
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0482 import stanza
from slixmpp.plugins.xep_0482.stanza import External, Jingle
from slixmpp.xmlstream import register_stanza_plugin
class TestCallInviteStanza(SlixTest):
def setUp(self):
stanza.register_plugins()
def test_invite(self):
"""Test that the element is created correctly."""
msg = Message()
msg['call-invite']['video'] = True
jingle = Jingle()
jingle['sid'] = 'toto'
jingle['jid'] = JID('toto@example.com/m')
external = External()
external['uri'] = "https://example.com/call"
msg['call-invite'].append(jingle)
msg['call-invite'].append(external)
self.check(msg, """
<message>
<invite xmlns="urn:xmpp:call-invites:0" video="true">
<jingle sid="toto" jid="toto@example.com/m" />
<external uri="https://example.com/call" />
</invite>
</message>
""")
self.assertEqual(
msg['call-invite'].get_methods(),
([jingle], [external]),
)
suite = unittest.TestLoader().loadTestsFromTestCase(TestCallInviteStanza)

View File

@ -1,178 +0,0 @@
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2025 nicoco
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import unittest
from slixmpp import register_stanza_plugin, ElementBase
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0492 import stanza
from slixmpp.plugins.xep_0402 import stanza as b_stanza
class TestNotificationSetting(SlixTest):
def setUp(self):
b_stanza.register_plugin()
stanza.register_plugin()
def test_never(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_always(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("always")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_on_mention(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("on-mention")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<on-mention />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_advanced(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
register_stanza_plugin(stanza.Advanced, AdvancedExtension)
bookmark["extensions"]["notify"]["advanced"].enable("cool")
bookmark["extensions"]["notify"]["advanced"]["cool"]["attrib"] = "cool-attrib"
bookmark["extensions"]["notify"]["advanced"]["cool"]["content"] = "cool-content"
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never client-type="pc" />
<on-mention client-type="mobile" />
<advanced>
<cool xmlns="cool-ns" attrib="cool-attrib">cool-content</cool>
</advanced>
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_change_config(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<never />
<never client-type="pc" />
<on-mention client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
bookmark["extensions"]["notify"].configure("always")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
<never client-type="pc" />
<on-mention client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
bookmark["extensions"]["notify"].configure("always", "mobile")
self.check(
bookmark,
"""
<conference xmlns='urn:xmpp:bookmarks:1'>
<extensions>
<notify xmlns='urn:xmpp:notification-settings:0'>
<always />
<never client-type="pc" />
<always client-type="mobile" />
</notify>
</extensions>
</conference>
""",
use_values=False,
)
def test_get_config(self):
bookmark = b_stanza.Conference()
bookmark["extensions"]["notify"].configure("never")
bookmark["extensions"]["notify"].configure("never", client_type="pc")
bookmark["extensions"]["notify"].configure("on-mention", client_type="mobile")
self.assertEqual(bookmark["extensions"]["notify"].get_config(), "never")
self.assertEqual(bookmark["extensions"]["notify"].get_config("pc"), "never")
self.assertEqual(
bookmark["extensions"]["notify"].get_config("mobile"), "on-mention"
)
class AdvancedExtension(ElementBase):
namespace = "cool-ns"
name = "cool"
plugin_attrib = name
interfaces = {"attrib", "content"}
def set_content(self, content: str):
self.xml.text = content
suite = unittest.TestLoader().loadTestsFromTestCase(TestNotificationSetting)

View File

@ -1,28 +0,0 @@
import unittest
from slixmpp.test import SlixTest
class TestPrivatePEP(SlixTest):
def testConfigureNode(self):
self.stream_start(mode='client', plugins=['xep_0223'])
self.xmpp.plugin['xep_0223'].configure(node="toto")
self.send("""
<iq type="set" id="1">
<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
<configure node='toto'>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field var='pubsub#persist_items'><value>1</value></field>
<field var='pubsub#access_model'><value>whitelist</value></field>
</x>
</configure>
</pubsub>
</iq>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestPrivatePEP)

View File

@ -1,53 +0,0 @@
import unittest
from slixmpp.jid import JID
from slixmpp.test import SlixTest
class TestStreamCorrect(SlixTest):
def test_recv_correct(self):
self.stream_start(mode='client', plugins=['xep_0308'])
recv = []
def recv_correct(msg):
recv.append(msg)
self.xmpp.add_event_handler('message_correction', recv_correct)
self.recv("""
<message from="example.com" to="toto@example">
<replace xmlns="urn:xmpp:message-correct:0" id="tototo"/>
<body>oucou</body>
</message>
""")
received = recv[0]
self.assertEqual(received['replace']['id'], "tototo")
def test_send_correct(self):
self.stream_start(mode='client', plugins=['xep_0308'])
corrected = self.xmpp.plugin['xep_0308'].build_correction(
id_to_replace="12345",
mto=JID('toto@example.com'),
mbody="I am replacing",
)
self.assertEqual(corrected['replace']['id'], '12345')
self.assertEqual(corrected['to'], JID('toto@example.com'))
self.assertEqual(corrected['body'], 'I am replacing')
corrected['id'] = 'my id'
corrected = self.xmpp.plugin['xep_0308'].correct_message(
corrected,
'This is new',
)
self.send("""
<message type="chat" to="toto@example.com">
<body>This is new</body>
<replace xmlns="urn:xmpp:message-correct:0" id="my id" />
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamCorrect)

View File

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

View File

@ -1,135 +0,0 @@
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)

5
tox.ini Normal file
View File

@ -0,0 +1,5 @@
[tox]
envlist = py34
[testenv]
deps = nose
commands = nosetests --where=tests --exclude=live -i slixtest.py