Compare commits
	
		
			47 Commits
		
	
	
		
			test-ci
			...
			xep-446-co
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					04244ecf82 | ||
| 
						 | 
					8c8bb5da8b | ||
| 
						 | 
					bd638f1b39 | ||
| 
						 | 
					0ff9e3661d | ||
| 
						 | 
					5ec378cccd | ||
| 
						 | 
					a9fc955eda | ||
| 
						 | 
					05860f71ac | ||
| 
						 | 
					1482bcc395 | ||
| 
						 | 
					2e736bc715 | ||
| 
						 | 
					8d984cd8a1 | ||
| 
						 | 
					100014651c | ||
| 
						 | 
					f9a9a0dcb7 | ||
| 
						 | 
					c585ec5983 | ||
| 
						 | 
					27bbb1ef95 | ||
| 
						 | 
					5dfc622539 | ||
| 
						 | 
					2ab9b5a05c | ||
| 
						 | 
					09d9320b91 | ||
| 
						 | 
					fbf298c36d | ||
| 
						 | 
					7153d79006 | ||
| 
						 | 
					1d3e03a923 | ||
| 
						 | 
					3d0b09e2e2 | ||
| 
						 | 
					23544731ef | ||
| 
						 | 
					a18a6c4eb8 | ||
| 
						 | 
					dd903b1792 | ||
| 
						 | 
					cf3b30120e | ||
| 
						 | 
					d86dccaf85 | ||
| 
						 | 
					075812adf3 | ||
| 
						 | 
					8955ece461 | ||
| 
						 | 
					5051c60262 | ||
| 
						 | 
					c495eb73fc | ||
| 
						 | 
					12c516d365 | ||
| 
						 | 
					d9b0b6dfe6 | ||
| 
						 | 
					7979e3b603 | ||
| 
						 | 
					f24a7679e5 | ||
| 
						 | 
					df0ecfc142 | ||
| 
						 | 
					e79b98b266 | ||
| 
						 | 
					5ed5e60b20 | ||
| 
						 | 
					e5fe53ef45 | ||
| 
						 | 
					93608bd2f4 | ||
| 
						 | 
					3b2386ee2f | ||
| 
						 | 
					b94c6716f7 | ||
| 
						 | 
					db8ce9187c | ||
| 
						 | 
					7f926a944a | ||
| 
						 | 
					e96f8e1ed0 | ||
| 
						 | 
					c8c0bb9134 | ||
| 
						 | 
					825c51b87d | ||
| 
						 | 
					7c79f28587 | 
							
								
								
									
										13
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							@@ -1,13 +0,0 @@
 | 
			
		||||
################ Please use Gitlab instead of Github ###################################
 | 
			
		||||
 | 
			
		||||
Hello, thank you for contributing to slixmpp!
 | 
			
		||||
 | 
			
		||||
You’re about to open a pull request on github. However this github repository is not the official place for contributions on slixmpp.
 | 
			
		||||
 | 
			
		||||
Please open your merge request on https://lab.louiz.org/poezio/slixmpp/
 | 
			
		||||
 | 
			
		||||
You should be able to log in there with your github credentials, clone the slixmpp repository in your namespace, push your existing pull request into a new branch, and then open a merge request with one click, within 3 minutes.
 | 
			
		||||
 | 
			
		||||
This will help us review your contribution, avoid spreading things everywhere and it will even run the tests automatically with your changes.
 | 
			
		||||
 | 
			
		||||
Thank you.
 | 
			
		||||
@@ -1,81 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
language: python
 | 
			
		||||
python:
 | 
			
		||||
  - "3.7"
 | 
			
		||||
  - "3.8-dev"
 | 
			
		||||
install:
 | 
			
		||||
  - "pip install ."
 | 
			
		||||
script: testall.py
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
when:
 | 
			
		||||
  event: [ push, pull_request ]
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  mypy:
 | 
			
		||||
    image: python:3
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,20 @@
 | 
			
		||||
when:
 | 
			
		||||
  event: [ push, pull_request ]
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  test_integration:
 | 
			
		||||
    image: "python:3.11"
 | 
			
		||||
    secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server]
 | 
			
		||||
    environment:
 | 
			
		||||
      CI_ACCOUNT1:
 | 
			
		||||
        from_secret: ci_account1
 | 
			
		||||
      CI_ACCOUNT1_PASSWORD:
 | 
			
		||||
        from_secret: ci_account1_password
 | 
			
		||||
      CI_ACCOUNT2:
 | 
			
		||||
        from_secret: ci_account2
 | 
			
		||||
      CI_ACCOUNT2_PASSWORD:
 | 
			
		||||
        from_secret: ci_account2_password
 | 
			
		||||
      CI_MUC_SERVER:
 | 
			
		||||
        from_secret: ci_muc_server
 | 
			
		||||
    commands:
 | 
			
		||||
      - apt-get update
 | 
			
		||||
      - apt-get install -y python3-pip cython3 gpg idn libidn-dev
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,19 @@
 | 
			
		||||
when:
 | 
			
		||||
  event: [ push, pull_request ]
 | 
			
		||||
 | 
			
		||||
steps:
 | 
			
		||||
  unit_tests:
 | 
			
		||||
    image: "python:${TAG}"
 | 
			
		||||
    commands:
 | 
			
		||||
    - apt-get update
 | 
			
		||||
    - apt-get install -y python3 python3-pip cython3 gpg
 | 
			
		||||
    - pip3 install emoji aiohttp cryptography
 | 
			
		||||
    - pip3 install emoji aiohttp cryptography setuptools
 | 
			
		||||
    - ./run_tests.py
 | 
			
		||||
 | 
			
		||||
matrix:
 | 
			
		||||
  TAG:
 | 
			
		||||
    - "3.7"
 | 
			
		||||
    - "3.9"
 | 
			
		||||
    - "3.8"
 | 
			
		||||
    - "3.10"
 | 
			
		||||
    - "3.11"
 | 
			
		||||
    - "3.12"
 | 
			
		||||
    - "3.13"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								doap.xml
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								doap.xml
									
									
									
									
									
								
							@@ -616,6 +616,14 @@
 | 
			
		||||
            <xmpp:since>1.0</xmpp:since>
 | 
			
		||||
        </xmpp:SupportedXep>
 | 
			
		||||
    </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>
 | 
			
		||||
        <xmpp:SupportedXep>
 | 
			
		||||
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0270.html"/>
 | 
			
		||||
@@ -682,6 +690,14 @@
 | 
			
		||||
            <xmpp:since>1.0</xmpp:since>
 | 
			
		||||
        </xmpp:SupportedXep>
 | 
			
		||||
    </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>
 | 
			
		||||
        <xmpp:SupportedXep>
 | 
			
		||||
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
 | 
			
		||||
@@ -856,7 +872,7 @@
 | 
			
		||||
        <xmpp:SupportedXep>
 | 
			
		||||
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html"/>
 | 
			
		||||
            <xmpp:status>complete</xmpp:status>
 | 
			
		||||
            <xmpp:version>0.3.0</xmpp:version>
 | 
			
		||||
            <xmpp:version>0.4.0</xmpp:version>
 | 
			
		||||
            <xmpp:since>1.6.0</xmpp:since>
 | 
			
		||||
        </xmpp:SupportedXep>
 | 
			
		||||
    </implements>
 | 
			
		||||
@@ -864,7 +880,7 @@
 | 
			
		||||
        <xmpp:SupportedXep>
 | 
			
		||||
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/>
 | 
			
		||||
            <xmpp:status>complete</xmpp:status>
 | 
			
		||||
            <xmpp:version>0.2.1</xmpp:version>
 | 
			
		||||
            <xmpp:version>0.3.0</xmpp:version>
 | 
			
		||||
            <xmpp:since>1.6.0</xmpp:since>
 | 
			
		||||
        </xmpp:SupportedXep>
 | 
			
		||||
    </implements>
 | 
			
		||||
@@ -900,13 +916,45 @@
 | 
			
		||||
            <xmpp:since>1.6.0</xmpp:since>
 | 
			
		||||
        </xmpp:SupportedXep>
 | 
			
		||||
    </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>
 | 
			
		||||
        <xmpp:SupportedXep>
 | 
			
		||||
            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
 | 
			
		||||
            <xmpp:status>partial</xmpp:status>
 | 
			
		||||
            <xmpp:version>0.1.0</xmpp:version>
 | 
			
		||||
            <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-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>
 | 
			
		||||
    </implements>
 | 
			
		||||
 | 
			
		||||
@@ -1071,5 +1119,12 @@
 | 
			
		||||
            <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/>
 | 
			
		||||
        </Version>
 | 
			
		||||
    </release>
 | 
			
		||||
    <release>
 | 
			
		||||
        <Version>
 | 
			
		||||
            <revision>1.8.6</revision>
 | 
			
		||||
            <created>2024-12-26</created>
 | 
			
		||||
            <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.6.tar.gz"/>
 | 
			
		||||
        </Version>
 | 
			
		||||
    </release>
 | 
			
		||||
</Project>
 | 
			
		||||
</rdf:RDF>
 | 
			
		||||
 
 | 
			
		||||
@@ -94,3 +94,4 @@ Plugin index
 | 
			
		||||
    xep_0439
 | 
			
		||||
    xep_0441
 | 
			
		||||
    xep_0444
 | 
			
		||||
    xep_0492
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								docs/api/plugins/xep_0492.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/api/plugins/xep_0492.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||
@@ -167,8 +167,9 @@ processing the same stanza twice.
 | 
			
		||||
        - **Data:** :py:class:`~.Message`
 | 
			
		||||
        - **Source:** :py:class:`BaseXMPP <.BaseXMPP>`
 | 
			
		||||
 | 
			
		||||
        Makes the contents of message stanzas available whenever one is received. Be
 | 
			
		||||
        sure to check the message type in order to handle error messages.
 | 
			
		||||
        Makes the contents of message stanzas that include <body> tags available
 | 
			
		||||
        whenever one is received.
 | 
			
		||||
        Be sure to check the message type to handle error messages appropriately.
 | 
			
		||||
 | 
			
		||||
    message_error
 | 
			
		||||
        - **Data:** :py:class:`~.Message`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
Projects Using Slixmpp
 | 
			
		||||
======================
 | 
			
		||||
 | 
			
		||||
This page enumerates software in the form of applications, bots and gateways utilizing the XMPP protocols with slixmpp.
 | 
			
		||||
 | 
			
		||||
Applications
 | 
			
		||||
------------
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +10,8 @@ sendxmpp-py
 | 
			
		||||
~~~~~~~~~~~
 | 
			
		||||
sendxmpp is a command line program and is the XMPP equivalent of sendmail. It is a Python version of the original sendxmpp which is written in Perl.
 | 
			
		||||
 | 
			
		||||
- `Source <https://github.com/moparisthebest/sendxmpp-py>`_
 | 
			
		||||
- `Source <https://code.moparisthebest.com/moparisthebest/sendxmpp-py>`_
 | 
			
		||||
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`_
 | 
			
		||||
 | 
			
		||||
Bots
 | 
			
		||||
----
 | 
			
		||||
@@ -19,6 +22,12 @@ XMPP bot which logs groupchat messages. Logs are in text format, with one file p
 | 
			
		||||
 | 
			
		||||
- `Source <https://git.khaganat.net/khaganat/BotLogMauve>`_
 | 
			
		||||
 | 
			
		||||
BukuBot
 | 
			
		||||
~~~~~~~
 | 
			
		||||
BukuBot makes it possible to manage and search your bookmarks from your chat.
 | 
			
		||||
 | 
			
		||||
- `Source <https://codeberg.org/sch/BukuBot>`_
 | 
			
		||||
 | 
			
		||||
LinkBot
 | 
			
		||||
~~~~~~~
 | 
			
		||||
This bot reveals the title of any shared link in a groupchat for quick content insight.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										184
									
								
								examples/imghdr.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								examples/imghdr.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
"""
 | 
			
		||||
Recognize image file formats based on their first few bytes.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Taken from cpython 3.11 source code before the removal in 3.13.
 | 
			
		||||
 | 
			
		||||
Licensed under Zero-Clause BSD
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from os import PathLike
 | 
			
		||||
import warnings
 | 
			
		||||
 | 
			
		||||
__all__ = ["what"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
warnings._deprecated(__name__, remove=(3, 13))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#-------------------------#
 | 
			
		||||
# Recognize image headers #
 | 
			
		||||
#-------------------------#
 | 
			
		||||
 | 
			
		||||
def what(file, h=None):
 | 
			
		||||
    f = None
 | 
			
		||||
    try:
 | 
			
		||||
        if h is None:
 | 
			
		||||
            if isinstance(file, (str, PathLike)):
 | 
			
		||||
                f = open(file, 'rb')
 | 
			
		||||
                h = f.read(32)
 | 
			
		||||
            else:
 | 
			
		||||
                location = file.tell()
 | 
			
		||||
                h = file.read(32)
 | 
			
		||||
                file.seek(location)
 | 
			
		||||
        for tf in tests:
 | 
			
		||||
            res = tf(h, f)
 | 
			
		||||
            if res:
 | 
			
		||||
                return res
 | 
			
		||||
    finally:
 | 
			
		||||
        if f: f.close()
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#---------------------------------#
 | 
			
		||||
# Subroutines per image file type #
 | 
			
		||||
#---------------------------------#
 | 
			
		||||
 | 
			
		||||
tests = []
 | 
			
		||||
 | 
			
		||||
def test_jpeg(h, f):
 | 
			
		||||
    """JPEG data with JFIF or Exif markers; and raw JPEG"""
 | 
			
		||||
    if h[6:10] in (b'JFIF', b'Exif'):
 | 
			
		||||
        return 'jpeg'
 | 
			
		||||
    elif h[:4] == b'\xff\xd8\xff\xdb':
 | 
			
		||||
        return 'jpeg'
 | 
			
		||||
 | 
			
		||||
tests.append(test_jpeg)
 | 
			
		||||
 | 
			
		||||
def test_png(h, f):
 | 
			
		||||
    if h.startswith(b'\211PNG\r\n\032\n'):
 | 
			
		||||
        return 'png'
 | 
			
		||||
 | 
			
		||||
tests.append(test_png)
 | 
			
		||||
 | 
			
		||||
def test_gif(h, f):
 | 
			
		||||
    """GIF ('87 and '89 variants)"""
 | 
			
		||||
    if h[:6] in (b'GIF87a', b'GIF89a'):
 | 
			
		||||
        return 'gif'
 | 
			
		||||
 | 
			
		||||
tests.append(test_gif)
 | 
			
		||||
 | 
			
		||||
def test_tiff(h, f):
 | 
			
		||||
    """TIFF (can be in Motorola or Intel byte order)"""
 | 
			
		||||
    if h[:2] in (b'MM', b'II'):
 | 
			
		||||
        return 'tiff'
 | 
			
		||||
 | 
			
		||||
tests.append(test_tiff)
 | 
			
		||||
 | 
			
		||||
def test_rgb(h, f):
 | 
			
		||||
    """SGI image library"""
 | 
			
		||||
    if h.startswith(b'\001\332'):
 | 
			
		||||
        return 'rgb'
 | 
			
		||||
 | 
			
		||||
tests.append(test_rgb)
 | 
			
		||||
 | 
			
		||||
def test_pbm(h, f):
 | 
			
		||||
    """PBM (portable bitmap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'pbm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_pbm)
 | 
			
		||||
 | 
			
		||||
def test_pgm(h, f):
 | 
			
		||||
    """PGM (portable graymap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'pgm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_pgm)
 | 
			
		||||
 | 
			
		||||
def test_ppm(h, f):
 | 
			
		||||
    """PPM (portable pixmap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'ppm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_ppm)
 | 
			
		||||
 | 
			
		||||
def test_rast(h, f):
 | 
			
		||||
    """Sun raster file"""
 | 
			
		||||
    if h.startswith(b'\x59\xA6\x6A\x95'):
 | 
			
		||||
        return 'rast'
 | 
			
		||||
 | 
			
		||||
tests.append(test_rast)
 | 
			
		||||
 | 
			
		||||
def test_xbm(h, f):
 | 
			
		||||
    """X bitmap (X10 or X11)"""
 | 
			
		||||
    if h.startswith(b'#define '):
 | 
			
		||||
        return 'xbm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_xbm)
 | 
			
		||||
 | 
			
		||||
def test_bmp(h, f):
 | 
			
		||||
    if h.startswith(b'BM'):
 | 
			
		||||
        return 'bmp'
 | 
			
		||||
 | 
			
		||||
tests.append(test_bmp)
 | 
			
		||||
 | 
			
		||||
def test_webp(h, f):
 | 
			
		||||
    if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
 | 
			
		||||
        return 'webp'
 | 
			
		||||
 | 
			
		||||
tests.append(test_webp)
 | 
			
		||||
 | 
			
		||||
def test_exr(h, f):
 | 
			
		||||
    if h.startswith(b'\x76\x2f\x31\x01'):
 | 
			
		||||
        return 'exr'
 | 
			
		||||
 | 
			
		||||
tests.append(test_exr)
 | 
			
		||||
 | 
			
		||||
#--------------------#
 | 
			
		||||
# Small test program #
 | 
			
		||||
#--------------------#
 | 
			
		||||
 | 
			
		||||
def test():
 | 
			
		||||
    import sys
 | 
			
		||||
    recursive = 0
 | 
			
		||||
    if sys.argv[1:] and sys.argv[1] == '-r':
 | 
			
		||||
        del sys.argv[1:2]
 | 
			
		||||
        recursive = 1
 | 
			
		||||
    try:
 | 
			
		||||
        if sys.argv[1:]:
 | 
			
		||||
            testall(sys.argv[1:], recursive, 1)
 | 
			
		||||
        else:
 | 
			
		||||
            testall(['.'], recursive, 1)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        sys.stderr.write('\n[Interrupted]\n')
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
def testall(list, recursive, toplevel):
 | 
			
		||||
    import sys
 | 
			
		||||
    import os
 | 
			
		||||
    for filename in list:
 | 
			
		||||
        if os.path.isdir(filename):
 | 
			
		||||
            print(filename + '/:', end=' ')
 | 
			
		||||
            if recursive or toplevel:
 | 
			
		||||
                print('recursing down:')
 | 
			
		||||
                import glob
 | 
			
		||||
                names = glob.glob(os.path.join(glob.escape(filename), '*'))
 | 
			
		||||
                testall(names, recursive, 0)
 | 
			
		||||
            else:
 | 
			
		||||
                print('*** directory (use -r) ***')
 | 
			
		||||
        else:
 | 
			
		||||
            print(filename + ':', end=' ')
 | 
			
		||||
            sys.stdout.flush()
 | 
			
		||||
            try:
 | 
			
		||||
                print(what(filename))
 | 
			
		||||
            except OSError:
 | 
			
		||||
                print('*** not found ***')
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    test()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										184
									
								
								itests/imghdr.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								itests/imghdr.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
"""
 | 
			
		||||
Recognize image file formats based on their first few bytes.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Taken from cpython 3.11 source code before the removal in 3.13.
 | 
			
		||||
 | 
			
		||||
Licensed under Zero-Clause BSD
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from os import PathLike
 | 
			
		||||
import warnings
 | 
			
		||||
 | 
			
		||||
__all__ = ["what"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
warnings._deprecated(__name__, remove=(3, 13))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#-------------------------#
 | 
			
		||||
# Recognize image headers #
 | 
			
		||||
#-------------------------#
 | 
			
		||||
 | 
			
		||||
def what(file, h=None):
 | 
			
		||||
    f = None
 | 
			
		||||
    try:
 | 
			
		||||
        if h is None:
 | 
			
		||||
            if isinstance(file, (str, PathLike)):
 | 
			
		||||
                f = open(file, 'rb')
 | 
			
		||||
                h = f.read(32)
 | 
			
		||||
            else:
 | 
			
		||||
                location = file.tell()
 | 
			
		||||
                h = file.read(32)
 | 
			
		||||
                file.seek(location)
 | 
			
		||||
        for tf in tests:
 | 
			
		||||
            res = tf(h, f)
 | 
			
		||||
            if res:
 | 
			
		||||
                return res
 | 
			
		||||
    finally:
 | 
			
		||||
        if f: f.close()
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#---------------------------------#
 | 
			
		||||
# Subroutines per image file type #
 | 
			
		||||
#---------------------------------#
 | 
			
		||||
 | 
			
		||||
tests = []
 | 
			
		||||
 | 
			
		||||
def test_jpeg(h, f):
 | 
			
		||||
    """JPEG data with JFIF or Exif markers; and raw JPEG"""
 | 
			
		||||
    if h[6:10] in (b'JFIF', b'Exif'):
 | 
			
		||||
        return 'jpeg'
 | 
			
		||||
    elif h[:4] == b'\xff\xd8\xff\xdb':
 | 
			
		||||
        return 'jpeg'
 | 
			
		||||
 | 
			
		||||
tests.append(test_jpeg)
 | 
			
		||||
 | 
			
		||||
def test_png(h, f):
 | 
			
		||||
    if h.startswith(b'\211PNG\r\n\032\n'):
 | 
			
		||||
        return 'png'
 | 
			
		||||
 | 
			
		||||
tests.append(test_png)
 | 
			
		||||
 | 
			
		||||
def test_gif(h, f):
 | 
			
		||||
    """GIF ('87 and '89 variants)"""
 | 
			
		||||
    if h[:6] in (b'GIF87a', b'GIF89a'):
 | 
			
		||||
        return 'gif'
 | 
			
		||||
 | 
			
		||||
tests.append(test_gif)
 | 
			
		||||
 | 
			
		||||
def test_tiff(h, f):
 | 
			
		||||
    """TIFF (can be in Motorola or Intel byte order)"""
 | 
			
		||||
    if h[:2] in (b'MM', b'II'):
 | 
			
		||||
        return 'tiff'
 | 
			
		||||
 | 
			
		||||
tests.append(test_tiff)
 | 
			
		||||
 | 
			
		||||
def test_rgb(h, f):
 | 
			
		||||
    """SGI image library"""
 | 
			
		||||
    if h.startswith(b'\001\332'):
 | 
			
		||||
        return 'rgb'
 | 
			
		||||
 | 
			
		||||
tests.append(test_rgb)
 | 
			
		||||
 | 
			
		||||
def test_pbm(h, f):
 | 
			
		||||
    """PBM (portable bitmap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'pbm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_pbm)
 | 
			
		||||
 | 
			
		||||
def test_pgm(h, f):
 | 
			
		||||
    """PGM (portable graymap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'pgm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_pgm)
 | 
			
		||||
 | 
			
		||||
def test_ppm(h, f):
 | 
			
		||||
    """PPM (portable pixmap)"""
 | 
			
		||||
    if len(h) >= 3 and \
 | 
			
		||||
        h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
 | 
			
		||||
        return 'ppm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_ppm)
 | 
			
		||||
 | 
			
		||||
def test_rast(h, f):
 | 
			
		||||
    """Sun raster file"""
 | 
			
		||||
    if h.startswith(b'\x59\xA6\x6A\x95'):
 | 
			
		||||
        return 'rast'
 | 
			
		||||
 | 
			
		||||
tests.append(test_rast)
 | 
			
		||||
 | 
			
		||||
def test_xbm(h, f):
 | 
			
		||||
    """X bitmap (X10 or X11)"""
 | 
			
		||||
    if h.startswith(b'#define '):
 | 
			
		||||
        return 'xbm'
 | 
			
		||||
 | 
			
		||||
tests.append(test_xbm)
 | 
			
		||||
 | 
			
		||||
def test_bmp(h, f):
 | 
			
		||||
    if h.startswith(b'BM'):
 | 
			
		||||
        return 'bmp'
 | 
			
		||||
 | 
			
		||||
tests.append(test_bmp)
 | 
			
		||||
 | 
			
		||||
def test_webp(h, f):
 | 
			
		||||
    if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
 | 
			
		||||
        return 'webp'
 | 
			
		||||
 | 
			
		||||
tests.append(test_webp)
 | 
			
		||||
 | 
			
		||||
def test_exr(h, f):
 | 
			
		||||
    if h.startswith(b'\x76\x2f\x31\x01'):
 | 
			
		||||
        return 'exr'
 | 
			
		||||
 | 
			
		||||
tests.append(test_exr)
 | 
			
		||||
 | 
			
		||||
#--------------------#
 | 
			
		||||
# Small test program #
 | 
			
		||||
#--------------------#
 | 
			
		||||
 | 
			
		||||
def test():
 | 
			
		||||
    import sys
 | 
			
		||||
    recursive = 0
 | 
			
		||||
    if sys.argv[1:] and sys.argv[1] == '-r':
 | 
			
		||||
        del sys.argv[1:2]
 | 
			
		||||
        recursive = 1
 | 
			
		||||
    try:
 | 
			
		||||
        if sys.argv[1:]:
 | 
			
		||||
            testall(sys.argv[1:], recursive, 1)
 | 
			
		||||
        else:
 | 
			
		||||
            testall(['.'], recursive, 1)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        sys.stderr.write('\n[Interrupted]\n')
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
def testall(list, recursive, toplevel):
 | 
			
		||||
    import sys
 | 
			
		||||
    import os
 | 
			
		||||
    for filename in list:
 | 
			
		||||
        if os.path.isdir(filename):
 | 
			
		||||
            print(filename + '/:', end=' ')
 | 
			
		||||
            if recursive or toplevel:
 | 
			
		||||
                print('recursing down:')
 | 
			
		||||
                import glob
 | 
			
		||||
                names = glob.glob(os.path.join(glob.escape(filename), '*'))
 | 
			
		||||
                testall(names, recursive, 0)
 | 
			
		||||
            else:
 | 
			
		||||
                print('*** directory (use -r) ***')
 | 
			
		||||
        else:
 | 
			
		||||
            print(filename + ':', end=' ')
 | 
			
		||||
            sys.stdout.flush()
 | 
			
		||||
            try:
 | 
			
		||||
                print(what(filename))
 | 
			
		||||
            except OSError:
 | 
			
		||||
                print('*** not found ***')
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    test()
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,6 @@ class TestRetract(SlixIntegration):
 | 
			
		||||
            fallback_text='Twas a mistake',
 | 
			
		||||
        )
 | 
			
		||||
        msg = await self.clients[1].wait_until('message_retract')
 | 
			
		||||
        self.assertEqual(msg['apply_to']['id'], 'toto')
 | 
			
		||||
        self.assertTrue(msg['apply_to']['retract'])
 | 
			
		||||
        self.assertEqual(msg['retract']['id'], 'toto')
 | 
			
		||||
 | 
			
		||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestRetract)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import logging
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from distutils.core import Command
 | 
			
		||||
from setuptools import Command
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import logging
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from distutils.core import Command
 | 
			
		||||
from setuptools import Command
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								setup.py
									
									
									
									
									
								
							@@ -33,12 +33,17 @@ CLASSIFIERS = [
 | 
			
		||||
    'Programming Language :: Python :: 3.7',
 | 
			
		||||
    'Programming Language :: Python :: 3.8',
 | 
			
		||||
    'Programming Language :: Python :: 3.9',
 | 
			
		||||
    'Programming Language :: Python :: 3.10',
 | 
			
		||||
    'Programming Language :: Python :: 3.11',
 | 
			
		||||
    'Programming Language :: Python :: 3.12',
 | 
			
		||||
    'Programming Language :: Python :: 3.13',
 | 
			
		||||
    'Topic :: Internet :: XMPP',
 | 
			
		||||
    'Topic :: Software Development :: Libraries :: Python Modules',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_include(library_name, header):
 | 
			
		||||
    command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name]
 | 
			
		||||
    try:
 | 
			
		||||
@@ -59,6 +64,7 @@ def check_include(library_name, header):
 | 
			
		||||
            print('%s headers not found.' % library_name)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
 | 
			
		||||
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +93,7 @@ setup(
 | 
			
		||||
    packages=packages,
 | 
			
		||||
    ext_modules=ext_modules,
 | 
			
		||||
    install_requires=[
 | 
			
		||||
        'aiodns>=1.0',
 | 
			
		||||
        'aiodns >= 1.0; sys_platform=="linux" or sys_platform=="darwin"',
 | 
			
		||||
        'pyasn1',
 | 
			
		||||
        'pyasn1_modules',
 | 
			
		||||
        'typing_extensions; python_version < "3.8.0"',
 | 
			
		||||
 
 | 
			
		||||
@@ -27,3 +27,9 @@ from slixmpp.clientxmpp import ClientXMPP
 | 
			
		||||
from slixmpp.componentxmpp import ComponentXMPP
 | 
			
		||||
 | 
			
		||||
from slixmpp.version import __version__, __version_info__
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'Message', 'Presence', 'Iq', 'JID', 'InvalidJID', 'ET', 'ElementBase',
 | 
			
		||||
    'register_stanza_plugin', 'XMLStream', 'BaseXMPP', 'ClientXMPP', 'ComponentXMPP',
 | 
			
		||||
    '__version__', '__version_info__'
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -315,13 +315,12 @@ class BaseXMPP(XMLStream):
 | 
			
		||||
        pres['lang'] = self.default_lang
 | 
			
		||||
        return pres
 | 
			
		||||
 | 
			
		||||
    def make_iq(self, id: str = "0", ifrom: OptJidStr = None,
 | 
			
		||||
    def make_iq(self, id: Optional[str] = None, ifrom: OptJidStr = None,
 | 
			
		||||
                ito: OptJidStr = None, itype: Optional[IqTypes] = None,
 | 
			
		||||
                iquery: Optional[str] = None) -> stanza.Iq:
 | 
			
		||||
        """Create a new :class:`~.Iq` stanza with a given Id and from JID.
 | 
			
		||||
 | 
			
		||||
        :param id: An ideally unique ID value for this stanza thread.
 | 
			
		||||
                   Defaults to 0.
 | 
			
		||||
        :param ifrom: The from :class:`~.JID`
 | 
			
		||||
                      to use for this stanza.
 | 
			
		||||
        :param ito: The destination :class:`~.JID`
 | 
			
		||||
@@ -332,7 +331,8 @@ class BaseXMPP(XMLStream):
 | 
			
		||||
        :param iquery: Optional namespace for adding a query element.
 | 
			
		||||
        """
 | 
			
		||||
        iq = self.Iq()
 | 
			
		||||
        iq['id'] = str(id)
 | 
			
		||||
        if id is not None:
 | 
			
		||||
            iq['id'] = str(id)
 | 
			
		||||
        iq['to'] = ito
 | 
			
		||||
        iq['from'] = ifrom
 | 
			
		||||
        iq['type'] = itype
 | 
			
		||||
 
 | 
			
		||||
@@ -135,6 +135,7 @@ _DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = {
 | 
			
		||||
    "not-allowed": "cancel",
 | 
			
		||||
    "not-authorized": "auth",
 | 
			
		||||
    "payment-required": "auth",
 | 
			
		||||
    "policy-violation": "modify",
 | 
			
		||||
    "recipient-unavailable": "wait",
 | 
			
		||||
    "redirect": "modify",
 | 
			
		||||
    "registration-required": "auth",
 | 
			
		||||
 
 | 
			
		||||
@@ -112,15 +112,18 @@ PLUGINS = [
 | 
			
		||||
    'xep_0421',  # Anonymous unique occupant identifiers for MUCs
 | 
			
		||||
    'xep_0422',  # Message Fastening
 | 
			
		||||
    'xep_0424',  # Message Retraction
 | 
			
		||||
    'xep_0425',  # Message Moderation
 | 
			
		||||
    'xep_0425',  # Moderated Message Retraction
 | 
			
		||||
    'xep_0428',  # Message Fallback
 | 
			
		||||
    'xep_0437',  # Room Activity Indicators
 | 
			
		||||
    'xep_0439',  # Quick Response
 | 
			
		||||
    'xep_0441',  # Message Archive Management Preferences
 | 
			
		||||
    'xep_0444',  # Message Reactions
 | 
			
		||||
    'xep_0446',  # File metadata element
 | 
			
		||||
    'xep_0447',  # Stateless file sharing
 | 
			
		||||
    'xep_0461',  # Message Replies
 | 
			
		||||
    'xep_0469',  # Bookmarks Pinning
 | 
			
		||||
    'xep_0490',  # Message Displayed Synchronization
 | 
			
		||||
    'xep_0492',  # Chat Notification Settings
 | 
			
		||||
    # Meant to be imported by plugins
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,18 @@
 | 
			
		||||
# Part of Slixmpp: The Slick XMPP Library
 | 
			
		||||
# :copyright: (c) 2012 Nathanael C. Fritz
 | 
			
		||||
# :license: MIT, see LICENSE for more details
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import copy
 | 
			
		||||
import logging
 | 
			
		||||
import threading
 | 
			
		||||
 | 
			
		||||
from typing import Any, Dict, Set, ClassVar
 | 
			
		||||
from typing import Any, Dict, Set, ClassVar, Union, TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from slixmpp.clientxmpp import ClientXMPP
 | 
			
		||||
    from slixmpp.componentxmpp import ComponentXMPP
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -272,7 +276,7 @@ class BasePlugin(object):
 | 
			
		||||
    #: `plugin.config['foo']`.
 | 
			
		||||
    default_config: ClassVar[Dict[str, Any]] = {}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, xmpp, config=None):
 | 
			
		||||
    def __init__(self, xmpp: Union[ClientXMPP,ComponentXMPP], config=None):
 | 
			
		||||
        self.xmpp = xmpp
 | 
			
		||||
        if self.xmpp:
 | 
			
		||||
            self.api = self.xmpp.api.wrap(self.name)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
 | 
			
		||||
# Slixmpp: The Slick XMPP Library
 | 
			
		||||
# Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
 | 
			
		||||
# This file is part of Slixmpp.
 | 
			
		||||
# See the file LICENSE for copying permission.
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from slixmpp.xmlstream import ElementBase, ET
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +79,14 @@ class FormField(ElementBase):
 | 
			
		||||
        reqXML = self.xml.find('{%s}required' % self.namespace)
 | 
			
		||||
        return reqXML is not None
 | 
			
		||||
 | 
			
		||||
    def get_value(self, convert=True):
 | 
			
		||||
    def get_value(self, convert=True, convert_list=False):
 | 
			
		||||
        """
 | 
			
		||||
        Gets the value for this field
 | 
			
		||||
 | 
			
		||||
        :param convert: Convert truthy values to boolean
 | 
			
		||||
        :param convert_list: Convert text-multi fields to a string with
 | 
			
		||||
            \n as separator for values
 | 
			
		||||
        """
 | 
			
		||||
        valsXML = self.xml.findall('{%s}value' % self.namespace)
 | 
			
		||||
        if len(valsXML) == 0:
 | 
			
		||||
            return None
 | 
			
		||||
@@ -92,7 +100,7 @@ class FormField(ElementBase):
 | 
			
		||||
                if valXML.text is None:
 | 
			
		||||
                    valXML.text = ''
 | 
			
		||||
                values.append(valXML.text)
 | 
			
		||||
            if self._type == 'text-multi' and convert:
 | 
			
		||||
            if self._type == 'text-multi' and convert_list:
 | 
			
		||||
                values = "\n".join(values)
 | 
			
		||||
            return values
 | 
			
		||||
        else:
 | 
			
		||||
@@ -127,6 +135,17 @@ class FormField(ElementBase):
 | 
			
		||||
        del self['value']
 | 
			
		||||
        valXMLName = '{%s}value' % self.namespace
 | 
			
		||||
 | 
			
		||||
        if not self._type:
 | 
			
		||||
            if isinstance(value, bool):
 | 
			
		||||
                log.debug("Passed a 'boolean' as value of an untyped field, assuming it is a 'boolean'")
 | 
			
		||||
                self._type = "boolean"
 | 
			
		||||
            elif isinstance(value, str):
 | 
			
		||||
                log.debug("Passed a 'str' as value of an untyped field, assuming it is a 'text-single'")
 | 
			
		||||
                self._type = "text-single"
 | 
			
		||||
            elif isinstance(value, (list, tuple)):
 | 
			
		||||
                log.debug("Passed a %s as value of an untyped field, assuming it is a 'text-multi'")
 | 
			
		||||
                self._type = "text-multi"
 | 
			
		||||
 | 
			
		||||
        if self._type == 'boolean':
 | 
			
		||||
            if value in self.true_values:
 | 
			
		||||
                valXML = ET.Element(valXMLName)
 | 
			
		||||
@@ -180,3 +199,6 @@ FormField.setOptions = FormField.set_options
 | 
			
		||||
FormField.setRequired = FormField.set_required
 | 
			
		||||
FormField.setTrue = FormField.set_true
 | 
			
		||||
FormField.setValue = FormField.set_value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 
 | 
			
		||||
@@ -165,11 +165,11 @@ class DiscoInfo(ElementBase):
 | 
			
		||||
            identities = []
 | 
			
		||||
        for id_xml in self.xml.findall('{%s}identity' % self.namespace):
 | 
			
		||||
            xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
 | 
			
		||||
            category = id_xml.attrib.get('category', None)
 | 
			
		||||
            type_ = id_xml.attrib.get('type', None)
 | 
			
		||||
            name = id_xml.attrib.get('name', None)
 | 
			
		||||
            if lang is None or xml_lang == lang:
 | 
			
		||||
                id = (id_xml.attrib['category'],
 | 
			
		||||
                      id_xml.attrib['type'],
 | 
			
		||||
                      id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
 | 
			
		||||
                      id_xml.attrib.get('name', None))
 | 
			
		||||
                id = (category, type_, xml_lang, name)
 | 
			
		||||
                if isinstance(identities, set):
 | 
			
		||||
                    identities.add(id)
 | 
			
		||||
                else:
 | 
			
		||||
@@ -253,10 +253,12 @@ class DiscoInfo(ElementBase):
 | 
			
		||||
        else:
 | 
			
		||||
            features = []
 | 
			
		||||
        for feature_xml in self.xml.findall('{%s}feature' % self.namespace):
 | 
			
		||||
            if isinstance(features, set):
 | 
			
		||||
                features.add(feature_xml.attrib['var'])
 | 
			
		||||
            else:
 | 
			
		||||
                features.append(feature_xml.attrib['var'])
 | 
			
		||||
            feature = feature_xml.attrib.get('var', None)
 | 
			
		||||
            if feature:
 | 
			
		||||
                if isinstance(features, set):
 | 
			
		||||
                    features.add(feature)
 | 
			
		||||
                else:
 | 
			
		||||
                    features.append(feature)
 | 
			
		||||
        return features
 | 
			
		||||
 | 
			
		||||
    def set_features(self, features: Iterable[str]):
 | 
			
		||||
 
 | 
			
		||||
@@ -49,11 +49,13 @@ from slixmpp.plugins.xep_0045.stanza import (
 | 
			
		||||
    MUCUserItem,
 | 
			
		||||
)
 | 
			
		||||
from slixmpp.types import (
 | 
			
		||||
    JidStr,
 | 
			
		||||
    MucRole,
 | 
			
		||||
    MucAffiliation,
 | 
			
		||||
    MucRoomItem,
 | 
			
		||||
    MucRoomItemKeys,
 | 
			
		||||
    PresenceArgs,
 | 
			
		||||
    PresenceShows,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
JoinResult = Tuple[Presence, Message, List[Presence], List[Message]]
 | 
			
		||||
@@ -187,7 +189,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
    def _handle_config_change(self, msg: Message):
 | 
			
		||||
        """Handle a MUC configuration change (with status code)."""
 | 
			
		||||
        self.xmpp.event('groupchat_config_status', msg)
 | 
			
		||||
        self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
 | 
			
		||||
        self.xmpp.event('muc::%s::config_status' % msg['from'].bare, msg)
 | 
			
		||||
 | 
			
		||||
    def _client_handle_presence(self, pr: Presence):
 | 
			
		||||
        """As a client, handle a presence stanza"""
 | 
			
		||||
@@ -264,7 +266,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
                            seconds: Optional[int] = None,
 | 
			
		||||
                            since: Optional[datetime] = None,
 | 
			
		||||
                            presence_options: Optional[PresenceArgs] = None,
 | 
			
		||||
                            timeout: Optional[int] = None) -> JoinResult:
 | 
			
		||||
                            timeout: int = 300) -> JoinResult:
 | 
			
		||||
        """
 | 
			
		||||
        Try to join a MUC and block until we are joined or get an error.
 | 
			
		||||
 | 
			
		||||
@@ -310,7 +312,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        stanza.send()
 | 
			
		||||
        return await self._await_join(room, timeout)
 | 
			
		||||
 | 
			
		||||
    async def _await_join(self, room: JID, timeout: Optional[int] = None) -> JoinResult:
 | 
			
		||||
    async def _await_join(self, room: JID, timeout: int = 300) -> JoinResult:
 | 
			
		||||
        """Do the heavy lifting for awaiting a MUC join
 | 
			
		||||
 | 
			
		||||
        A muc join, once the join stanza is sent, is:
 | 
			
		||||
@@ -358,7 +360,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        return (pres, subject, occupant_buffer, history_buffer)
 | 
			
		||||
 | 
			
		||||
    def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
 | 
			
		||||
                 pstatus='', pshow='', pfrom='') -> asyncio.Future:
 | 
			
		||||
                 pstatus='', pshow: PresenceShows='chat', pfrom: JidStr='') -> asyncio.Future:
 | 
			
		||||
        """ Join the specified room, requesting 'maxhistory' lines of history.
 | 
			
		||||
 | 
			
		||||
        .. deprecated:: 1.8.0
 | 
			
		||||
@@ -412,7 +414,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            )
 | 
			
		||||
        del self.rooms[room]
 | 
			
		||||
 | 
			
		||||
    def set_subject(self, room: JID, subject: str, *, mfrom: Optional[JID] = None):
 | 
			
		||||
    def set_subject(self, room: JidStr, subject: str, *, mfrom: Optional[JID] = None):
 | 
			
		||||
        """Set a room’s subject.
 | 
			
		||||
 | 
			
		||||
        :param room: JID of the room.
 | 
			
		||||
@@ -423,7 +425,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        msg['subject'] = subject
 | 
			
		||||
        msg.send()
 | 
			
		||||
 | 
			
		||||
    async def get_room_config(self, room: JID, ifrom: Optional[JID] = None,
 | 
			
		||||
    async def get_room_config(self, room: JidStr, ifrom: Optional[JID] = None,
 | 
			
		||||
                              **iqkwargs) -> Form:
 | 
			
		||||
        """Get the room config form in 0004 plugin format.
 | 
			
		||||
 | 
			
		||||
@@ -438,7 +440,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            raise ValueError("Configuration form not found")
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    async def set_room_config(self, room: JID, config: Form, *,
 | 
			
		||||
    async def set_room_config(self, room: JidStr, config: Form, *,
 | 
			
		||||
                              ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
        """Send a room config form.
 | 
			
		||||
 | 
			
		||||
@@ -451,8 +453,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def cancel_config(self, room: JID, *,
 | 
			
		||||
                            ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
    async def cancel_config(self, room: JidStr, *,
 | 
			
		||||
                            ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """Cancel a requested config form.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to cancel the form for.
 | 
			
		||||
@@ -462,8 +464,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def destroy(self, room: JID, reason: str = '', altroom: Optional[JID] = None, *,
 | 
			
		||||
                      ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
    async def destroy(self, room: JidStr, reason: str = '', altroom: Optional[JidStr] = None, *,
 | 
			
		||||
                      ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """Destroy a room.
 | 
			
		||||
 | 
			
		||||
        :param room: Room JID to destroy.
 | 
			
		||||
@@ -479,10 +481,10 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            iq['mucowner_query']['destroy']['reason'] = reason
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def set_affiliation(self, room: JID, affiliation: MucAffiliation, *,
 | 
			
		||||
                              jid: Optional[JID] = None,
 | 
			
		||||
    async def set_affiliation(self, room: JidStr, affiliation: MucAffiliation, *,
 | 
			
		||||
                              jid: Optional[JidStr] = None,
 | 
			
		||||
                              nick: Optional[str] = None, reason: str = '',
 | 
			
		||||
                              ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
                              ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """ Change room affiliation for a JID or nickname.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to modify.
 | 
			
		||||
@@ -493,7 +495,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        if affiliation not in AFFILIATIONS:
 | 
			
		||||
            raise ValueError('%s is not a valid affiliation' % affiliation)
 | 
			
		||||
        if affiliation == 'outcast' and not jid:
 | 
			
		||||
            raise ValueError('Outcast affiliation requires a using a jid')
 | 
			
		||||
            raise ValueError('Outcast affiliation requires using a jid')
 | 
			
		||||
        if not any((jid, nick)):
 | 
			
		||||
            raise ValueError('One of jid or nick must be set')
 | 
			
		||||
        iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
 | 
			
		||||
@@ -506,8 +508,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            iq['mucadmin_query']['item']['reason'] = reason
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def get_affiliation_list(self, room: JID, affiliation: MucAffiliation, *,
 | 
			
		||||
                                   ifrom: Optional[JID] = None, **iqkwargs) -> List[JID]:
 | 
			
		||||
    async def get_affiliation_list(self, room: JidStr, affiliation: MucAffiliation, *,
 | 
			
		||||
                                   ifrom: Optional[JidStr] = None, **iqkwargs) -> List[JID]:
 | 
			
		||||
        """Get a list of JIDs with the specified affiliation
 | 
			
		||||
 | 
			
		||||
        :param room: Room to get affiliations from.
 | 
			
		||||
@@ -518,9 +520,9 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        result = await iq.send(**iqkwargs)
 | 
			
		||||
        return [item['jid'] for item in result['mucadmin_query']]
 | 
			
		||||
 | 
			
		||||
    async def send_affiliation_list(self, room: JID,
 | 
			
		||||
                                    affiliations: List[Tuple[JID, MucAffiliation]], *,
 | 
			
		||||
                                    ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
    async def send_affiliation_list(self, room: JidStr,
 | 
			
		||||
                                    affiliations: List[Tuple[JidStr, MucAffiliation]], *,
 | 
			
		||||
                                    ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """Send an affiliation delta list.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to send the affiliations to.
 | 
			
		||||
@@ -534,8 +536,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            iq['mucadmin_query'].append(item)
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def set_role(self, room: JID, nick: str, role: MucRole, *,
 | 
			
		||||
                       reason: str = '', ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
    async def set_role(self, room: JidStr, nick: str, role: MucRole, *,
 | 
			
		||||
                       reason: str = '', ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """ Change role property of a nick in a room.
 | 
			
		||||
            Typically, roles are temporary (they last only as long as you are in the
 | 
			
		||||
            room), whereas affiliations are permanent (they last across groupchat
 | 
			
		||||
@@ -555,8 +557,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            iq['mucadmin_query']['item']['reason'] = reason
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    async def get_roles_list(self, room: JID, role: MucRole, *,
 | 
			
		||||
                             ifrom: Optional[JID] = None, **iqkwargs) -> List[str]:
 | 
			
		||||
    async def get_roles_list(self, room: JidStr, role: MucRole, *,
 | 
			
		||||
                             ifrom: Optional[JidStr] = None, **iqkwargs) -> List[str]:
 | 
			
		||||
        """"Get a list of JIDs with the specified role
 | 
			
		||||
 | 
			
		||||
        :param room: Room to get roles from.
 | 
			
		||||
@@ -567,8 +569,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        result = await iq.send(**iqkwargs)
 | 
			
		||||
        return [item['nick'] for item in result['mucadmin_query']]
 | 
			
		||||
 | 
			
		||||
    async def send_role_list(self, room: JID, roles: List[Tuple[str, MucRole]], *,
 | 
			
		||||
                             ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
    async def send_role_list(self, room: JidStr, roles: List[Tuple[str, MucRole]], *,
 | 
			
		||||
                             ifrom: Optional[JidStr] = None, **iqkwargs):
 | 
			
		||||
        """Send a role delta list.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to send the roles to.
 | 
			
		||||
@@ -582,8 +584,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            iq['mucadmin_query'].append(item)
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 | 
			
		||||
    def invite(self, room: JID, jid: JID, reason: str = '', *,
 | 
			
		||||
               mfrom: Optional[JID] = None):
 | 
			
		||||
    def invite(self, room: JidStr, jid: JidStr, reason: str = '', *,
 | 
			
		||||
               mfrom: Optional[JidStr] = None):
 | 
			
		||||
        """ Invite a jid to a room (mediated invitation).
 | 
			
		||||
 | 
			
		||||
        :param room: Room to invite the user in.
 | 
			
		||||
@@ -596,8 +598,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            msg['muc']['invite']['reason'] = reason
 | 
			
		||||
        self.xmpp.send(msg)
 | 
			
		||||
 | 
			
		||||
    def invite_server(self, room: JID, jid: JID,
 | 
			
		||||
                         invite_from: JID, reason: str = ''):
 | 
			
		||||
    def invite_server(self, room: JidStr, jid: JidStr,
 | 
			
		||||
                         invite_from: JidStr, reason: str = ''):
 | 
			
		||||
        """Send a mediated invite to a user, as a MUC service.
 | 
			
		||||
 | 
			
		||||
        .. versionadded:: 1.8.0
 | 
			
		||||
@@ -615,8 +617,8 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            msg['muc']['invite']['reason'] = reason
 | 
			
		||||
        msg.send()
 | 
			
		||||
 | 
			
		||||
    def decline(self, room: JID, jid: JID, reason: str = '', *,
 | 
			
		||||
                mfrom: Optional[JID] = None):
 | 
			
		||||
    def decline(self, room: JidStr, jid: JidStr, reason: str = '', *,
 | 
			
		||||
                mfrom: Optional[JidStr] = None):
 | 
			
		||||
        """Decline a mediated invitation.
 | 
			
		||||
 | 
			
		||||
        :param room: Room the invitation came from.
 | 
			
		||||
@@ -629,7 +631,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            msg['muc']['decline']['reason'] = reason
 | 
			
		||||
        self.xmpp.send(msg)
 | 
			
		||||
 | 
			
		||||
    def request_voice(self, room: JID, role: str, *, mfrom: Optional[JID] = None):
 | 
			
		||||
    def request_voice(self, room: JidStr, role: str, *, mfrom: Optional[JidStr] = None):
 | 
			
		||||
        """Request voice in a moderated room.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to request voice from.
 | 
			
		||||
@@ -646,29 +648,49 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
        """Check if a JID is present in a room.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to check.
 | 
			
		||||
        :param jid: JID to check.
 | 
			
		||||
        :param jid: FULL JID to check.
 | 
			
		||||
        """
 | 
			
		||||
        bare_match = False
 | 
			
		||||
        for nick in self.rooms[room]:
 | 
			
		||||
            entry = self.rooms[room][nick]
 | 
			
		||||
            if not entry.get('jid'):
 | 
			
		||||
                continue
 | 
			
		||||
            if entry is not None and entry['jid'].full == jid:
 | 
			
		||||
 | 
			
		||||
            if entry['jid'] == jid.full:
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
            elif JID(entry['jid']).bare == jid.bare:
 | 
			
		||||
                bare_match = True
 | 
			
		||||
        
 | 
			
		||||
        if bare_match:
 | 
			
		||||
            logging.info(
 | 
			
		||||
                "Could not retrieve full JID, falling back to bare JID for %s in %s",
 | 
			
		||||
                jid, room
 | 
			
		||||
            )
 | 
			
		||||
        return bare_match
 | 
			
		||||
 | 
			
		||||
    def get_nick(self, room: JID, jid: JID) -> Optional[str]:
 | 
			
		||||
        """Get the nickname of a specific JID in a room.
 | 
			
		||||
 | 
			
		||||
        :param room: Room to inspect.
 | 
			
		||||
        :param jid: JID whose nick to return.
 | 
			
		||||
        :param jid: FULL JID whose nick to return.
 | 
			
		||||
        """
 | 
			
		||||
        bare_match = None
 | 
			
		||||
        for nick in self.rooms[room]:
 | 
			
		||||
            entry = self.rooms[room][nick]
 | 
			
		||||
            if not entry.get('jid'):
 | 
			
		||||
                continue
 | 
			
		||||
            if entry is not None and entry['jid'].full == jid:
 | 
			
		||||
 | 
			
		||||
            if entry['jid'] == jid.full:
 | 
			
		||||
                return nick
 | 
			
		||||
        return None
 | 
			
		||||
            elif JID(entry['jid']).bare == jid.bare:
 | 
			
		||||
                bare_match = nick
 | 
			
		||||
        
 | 
			
		||||
        if bare_match:
 | 
			
		||||
            logging.info(
 | 
			
		||||
                "Could not retrieve full JID, falling back to bare JID for %s in %s",
 | 
			
		||||
                jid, room
 | 
			
		||||
            )
 | 
			
		||||
        return bare_match
 | 
			
		||||
 | 
			
		||||
    def get_joined_rooms(self) -> List[JID]:
 | 
			
		||||
        """Get the list of rooms we sent a join presence to
 | 
			
		||||
@@ -704,7 +726,7 @@ class XEP_0045(BasePlugin):
 | 
			
		||||
            raise ValueError("Room %s is not joined" % room)
 | 
			
		||||
        return list(self.rooms[room].keys())
 | 
			
		||||
 | 
			
		||||
    def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
 | 
			
		||||
    def get_users_by_affiliation(self, room: JidStr, affiliation='member', *, ifrom: Optional[JidStr] = None):
 | 
			
		||||
        # Preserve old API
 | 
			
		||||
        if affiliation not in AFFILIATIONS:
 | 
			
		||||
            raise ValueError("Affiliation %s does not exist" % affiliation)
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class MUCBase(ElementBase):
 | 
			
		||||
    plugin_attrib = 'muc'
 | 
			
		||||
    interfaces = {'affiliation', 'role', 'jid', 'nick', 'room', 'status_codes'}
 | 
			
		||||
 | 
			
		||||
    def get_status_codes(self) -> Set[str]:
 | 
			
		||||
    def get_status_codes(self) -> Set[int]:
 | 
			
		||||
        status = self.xml.findall(f'{{{NS_USER}}}status')
 | 
			
		||||
        return {int(status.attrib['code']) for status in status}
 | 
			
		||||
 | 
			
		||||
@@ -275,7 +275,8 @@ class MUCUserItem(ElementBase):
 | 
			
		||||
        jid = self.xml.attrib.get('jid', None)
 | 
			
		||||
        if jid:
 | 
			
		||||
            return JID(jid)
 | 
			
		||||
        return jid
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MUCActor(ElementBase):
 | 
			
		||||
@@ -288,7 +289,8 @@ class MUCActor(ElementBase):
 | 
			
		||||
        jid = self.xml.attrib.get('jid', None)
 | 
			
		||||
        if jid:
 | 
			
		||||
            return JID(jid)
 | 
			
		||||
        return jid
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MUCDestroy(ElementBase):
 | 
			
		||||
 
 | 
			
		||||
@@ -137,7 +137,14 @@ class XEP_0199(BasePlugin):
 | 
			
		||||
    async def _keepalive(self, event=None):
 | 
			
		||||
        log.debug("Keepalive ping...")
 | 
			
		||||
        try:
 | 
			
		||||
            rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
 | 
			
		||||
            ifrom = None
 | 
			
		||||
            if self.xmpp.is_component:
 | 
			
		||||
                ifrom = self.xmpp.boundjid
 | 
			
		||||
            rtt = await self.ping(
 | 
			
		||||
                self.xmpp.boundjid.host,
 | 
			
		||||
                timeout=self.timeout,
 | 
			
		||||
                ifrom=ifrom
 | 
			
		||||
            )
 | 
			
		||||
        except IqTimeout:
 | 
			
		||||
            log.debug("Did not receive ping back in time. " + \
 | 
			
		||||
                      "Requesting Reconnect.")
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,18 @@ class XEP_0223(BasePlugin):
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    XEP-0223: Persistent Storage of Private Data via PubSub
 | 
			
		||||
 | 
			
		||||
    If a specific pubsub node requires additional publish options, edit the
 | 
			
		||||
    :attr:`.node_profile` attribute of this plugin:
 | 
			
		||||
 | 
			
		||||
    .. code-block:: python
 | 
			
		||||
 | 
			
		||||
        self.xmpp.plugin["xep_0223"].node_profiles["urn:some:node"] = {
 | 
			
		||||
            "pubsub#max_items" = "max"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    This makes :meth:`.store` add these publish options whenever it is called
 | 
			
		||||
    for the ``urn:some:node`` node.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    name = 'xep_0223'
 | 
			
		||||
@@ -28,6 +40,7 @@ class XEP_0223(BasePlugin):
 | 
			
		||||
 | 
			
		||||
    profile = {'pubsub#persist_items': True,
 | 
			
		||||
               'pubsub#access_model': 'whitelist'}
 | 
			
		||||
    node_profiles = dict[str, dict[str, str]]()
 | 
			
		||||
 | 
			
		||||
    def configure(self, node: str, **iqkwargs) -> Future:
 | 
			
		||||
        """
 | 
			
		||||
@@ -70,7 +83,8 @@ class XEP_0223(BasePlugin):
 | 
			
		||||
                value='http://jabber.org/protocol/pubsub#publish-options')
 | 
			
		||||
 | 
			
		||||
        fields = options['fields']
 | 
			
		||||
        for field, value in self.profile.items():
 | 
			
		||||
        profile = self.profile | self.node_profiles.get(node, {})
 | 
			
		||||
        for field, value in profile.items():
 | 
			
		||||
            if field not in fields:
 | 
			
		||||
                options.add_field(var=field)
 | 
			
		||||
            options.get_fields()[field]['value'] = value
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ from asyncio import Future
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from slixmpp import JID
 | 
			
		||||
from slixmpp.exceptions import XMPPError
 | 
			
		||||
from slixmpp.stanza import Iq, Message, Presence
 | 
			
		||||
from slixmpp.xmlstream.handler import CoroutineCallback
 | 
			
		||||
from slixmpp.xmlstream.matcher import StanzaPath
 | 
			
		||||
@@ -139,6 +140,13 @@ class XEP_0231(BasePlugin):
 | 
			
		||||
            self.xmpp.event('bob', iq)
 | 
			
		||||
        elif iq['type'] == 'get':
 | 
			
		||||
            data = await self.api['get_bob'](iq['to'], None, iq['from'], args=cid)
 | 
			
		||||
 | 
			
		||||
            if data is None:
 | 
			
		||||
                raise XMPPError(
 | 
			
		||||
                    "item-not-found",
 | 
			
		||||
                    f"Bits of binary '{cid}' is not available",
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            if isinstance(data, Iq):
 | 
			
		||||
                data['id'] = iq['id']
 | 
			
		||||
                data.send()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
# See the file LICENSE for copying permissio
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import slixmpp
 | 
			
		||||
from slixmpp.stanza import Message
 | 
			
		||||
from slixmpp.xmlstream.handler import Callback
 | 
			
		||||
from slixmpp.xmlstream.matcher import StanzaPath
 | 
			
		||||
@@ -45,5 +44,8 @@ class XEP_0308(BasePlugin):
 | 
			
		||||
    def session_bind(self, jid):
 | 
			
		||||
        self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
 | 
			
		||||
 | 
			
		||||
    def _handle_correction(self, msg):
 | 
			
		||||
    def is_correction(self, msg: Message):
 | 
			
		||||
        return msg.xml.find('{%s}replace' % Replace.namespace) is not None
 | 
			
		||||
 | 
			
		||||
    def _handle_correction(self, msg: Message):
 | 
			
		||||
        self.xmpp.event('message_correction', msg)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
# This file is part of Slixmpp.
 | 
			
		||||
# See the file LICENSE for copying permission.
 | 
			
		||||
from slixmpp.plugins.base import register_plugin
 | 
			
		||||
from slixmpp.plugins.xep_0424.stanza import *
 | 
			
		||||
from slixmpp.plugins.xep_0424.retraction import XEP_0424
 | 
			
		||||
from .retraction import XEP_0424
 | 
			
		||||
 | 
			
		||||
register_plugin(XEP_0424)
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ class XEP_0424(BasePlugin):
 | 
			
		||||
        stanza.register_plugins()
 | 
			
		||||
        self.xmpp.register_handler(Callback(
 | 
			
		||||
            "Message Retracted",
 | 
			
		||||
            StanzaPath("message/apply_to/retract"),
 | 
			
		||||
            StanzaPath("message/retract"),
 | 
			
		||||
            self._handle_retract_message,
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +64,6 @@ class XEP_0424(BasePlugin):
 | 
			
		||||
        if include_fallback:
 | 
			
		||||
            msg['body'] = fallback_text
 | 
			
		||||
            msg.enable('fallback')
 | 
			
		||||
        msg['apply_to']['id'] = id
 | 
			
		||||
        msg['apply_to'].enable('retract')
 | 
			
		||||
        msg['retract']['id'] = id
 | 
			
		||||
        msg.enable('store')
 | 
			
		||||
        msg.send()
 | 
			
		||||
 
 | 
			
		||||
@@ -8,28 +8,27 @@ from slixmpp.xmlstream import (
 | 
			
		||||
    ElementBase,
 | 
			
		||||
    register_stanza_plugin,
 | 
			
		||||
)
 | 
			
		||||
from slixmpp.plugins.xep_0422.stanza import ApplyTo
 | 
			
		||||
from slixmpp.plugins.xep_0359 import OriginID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
NS = 'urn:xmpp:message-retract:0'
 | 
			
		||||
NS = 'urn:xmpp:message-retract:1'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Retract(ElementBase):
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    name = 'retract'
 | 
			
		||||
    plugin_attrib = 'retract'
 | 
			
		||||
    interfaces = {'reason', 'id'}
 | 
			
		||||
    sub_interfaces = {'reason'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Retracted(ElementBase):
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    name = 'retracted'
 | 
			
		||||
    plugin_attrib = 'retracted'
 | 
			
		||||
    interfaces = {'stamp'}
 | 
			
		||||
    interfaces = {'stamp', 'id', 'reason'}
 | 
			
		||||
    sub_interfaces = {'reason'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_plugins():
 | 
			
		||||
    register_stanza_plugin(ApplyTo, Retract)
 | 
			
		||||
    register_stanza_plugin(Message, Retract)
 | 
			
		||||
    register_stanza_plugin(Message, Retracted)
 | 
			
		||||
 | 
			
		||||
    register_stanza_plugin(Retracted, OriginID)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@ from slixmpp.plugins.xep_0425 import stanza
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XEP_0425(BasePlugin):
 | 
			
		||||
    '''XEP-0425: Message Moderation'''
 | 
			
		||||
    '''XEP-0425: Moderated Message Retraction'''
 | 
			
		||||
 | 
			
		||||
    name = 'xep_0425'
 | 
			
		||||
    description = 'XEP-0425: Message Moderation'
 | 
			
		||||
    description = 'XEP-0425: Moderated Message Retraction'
 | 
			
		||||
    dependencies = {'xep_0424', 'xep_0421'}
 | 
			
		||||
    stanza = stanza
 | 
			
		||||
    namespace = stanza.NS
 | 
			
		||||
@@ -25,7 +25,7 @@ class XEP_0425(BasePlugin):
 | 
			
		||||
        stanza.register_plugins()
 | 
			
		||||
        self.xmpp.register_handler(Callback(
 | 
			
		||||
            'Moderated Message',
 | 
			
		||||
            StanzaPath('message/apply_to/moderated/retract'),
 | 
			
		||||
            StanzaPath('message/retract/moderated'),
 | 
			
		||||
            self._handle_moderated,
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +42,7 @@ class XEP_0425(BasePlugin):
 | 
			
		||||
    async def moderate(self, room: JID, id: str, reason: str = '', *,
 | 
			
		||||
                       ifrom: Optional[JID] = None, **iqkwargs):
 | 
			
		||||
        iq = self.xmpp.make_iq_set(ito=room.bare, ifrom=ifrom)
 | 
			
		||||
        iq['apply_to']['id'] = id
 | 
			
		||||
        iq['apply_to']['moderate']['reason'] = reason
 | 
			
		||||
        iq['apply_to']['moderate'].enable('retract')
 | 
			
		||||
        iq['moderate']['id'] = id
 | 
			
		||||
        iq['moderate']['reason'] = reason
 | 
			
		||||
        iq['moderate'].enable('retract')
 | 
			
		||||
        await iq.send(**iqkwargs)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,19 +8,18 @@ from slixmpp.xmlstream import (
 | 
			
		||||
    ElementBase,
 | 
			
		||||
    register_stanza_plugin,
 | 
			
		||||
)
 | 
			
		||||
from slixmpp.plugins.xep_0422.stanza import ApplyTo
 | 
			
		||||
from slixmpp.plugins.xep_0421.stanza import OccupantId
 | 
			
		||||
from slixmpp.plugins.xep_0424.stanza import Retract, Retracted
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
NS = 'urn:xmpp:message-moderate:0'
 | 
			
		||||
NS = 'urn:xmpp:message-moderate:1'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Moderate(ElementBase):
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    name = 'moderate'
 | 
			
		||||
    plugin_attrib = 'moderate'
 | 
			
		||||
    interfaces = {'reason'}
 | 
			
		||||
    interfaces = {'id', 'reason'}
 | 
			
		||||
    sub_interfaces = {'reason'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -28,17 +27,17 @@ class Moderated(ElementBase):
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    name = 'moderated'
 | 
			
		||||
    plugin_attrib = 'moderated'
 | 
			
		||||
    interfaces = {'reason', 'by'}
 | 
			
		||||
    sub_interfaces = {'reason'}
 | 
			
		||||
    interfaces = {'by'}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_plugins():
 | 
			
		||||
    register_stanza_plugin(Iq, ApplyTo)
 | 
			
		||||
    register_stanza_plugin(ApplyTo, Moderate)
 | 
			
		||||
    # for moderation requests
 | 
			
		||||
    register_stanza_plugin(Iq, Moderate)
 | 
			
		||||
    register_stanza_plugin(Moderate, Retract)
 | 
			
		||||
 | 
			
		||||
    register_stanza_plugin(Message, Moderated)
 | 
			
		||||
    register_stanza_plugin(ApplyTo, Moderated)
 | 
			
		||||
    register_stanza_plugin(Moderated, Retract)
 | 
			
		||||
    register_stanza_plugin(Moderated, Retracted)
 | 
			
		||||
    # for moderation events
 | 
			
		||||
    register_stanza_plugin(Retract, Moderated)
 | 
			
		||||
    register_stanza_plugin(Moderated, OccupantId)
 | 
			
		||||
 | 
			
		||||
    # for tombstones
 | 
			
		||||
    register_stanza_plugin(Retracted, Moderated)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,3 +18,7 @@ class XEP_0446(BasePlugin):
 | 
			
		||||
    name = "xep_0446"
 | 
			
		||||
    description = "XEP-0446: File metadata element"
 | 
			
		||||
    stanza = stanza
 | 
			
		||||
    dependencies = {'xep_0300', 'xep_0264'}
 | 
			
		||||
 | 
			
		||||
    def plugin_init(self):
 | 
			
		||||
        stanza.register_plugins()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from slixmpp.plugins.xep_0082 import format_datetime, parse
 | 
			
		||||
from slixmpp.xmlstream import ElementBase
 | 
			
		||||
from slixmpp.plugins.xep_0300 import Hash
 | 
			
		||||
from slixmpp.plugins.xep_0264.stanza import Thumbnail
 | 
			
		||||
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
 | 
			
		||||
 | 
			
		||||
NS = "urn:xmpp:file:metadata:0"
 | 
			
		||||
 | 
			
		||||
@@ -10,15 +13,42 @@ class File(ElementBase):
 | 
			
		||||
    name = "file"
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    plugin_attrib = "file"
 | 
			
		||||
    interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"}
 | 
			
		||||
    interfaces = sub_interfaces = {
 | 
			
		||||
        "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):
 | 
			
		||||
        self._set_sub_text("size", str(size))
 | 
			
		||||
        self.__set_if_positive("size", size)
 | 
			
		||||
 | 
			
		||||
    def get_size(self):
 | 
			
		||||
        return _int_or_none(self._get_sub_text("size"))
 | 
			
		||||
    def get_size(self) -> Optional[int]:
 | 
			
		||||
        return _positive_int_or_none(self._get_sub_text("size"))
 | 
			
		||||
 | 
			
		||||
    def get_date(self):
 | 
			
		||||
    def get_date(self) -> Optional[datetime]:
 | 
			
		||||
        try:
 | 
			
		||||
            return parse(self._get_sub_text("date"))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
@@ -30,9 +60,18 @@ class File(ElementBase):
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            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:
 | 
			
		||||
        return int(v)
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
def register_plugins():
 | 
			
		||||
    register_stanza_plugin(File, Hash)
 | 
			
		||||
    register_stanza_plugin(File, Thumbnail)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from slixmpp.plugins import BasePlugin
 | 
			
		||||
from slixmpp.types import JidStr
 | 
			
		||||
from slixmpp.xmlstream import StanzaBase
 | 
			
		||||
@@ -36,13 +38,35 @@ class XEP_0461(BasePlugin):
 | 
			
		||||
    def _handle_reply_to_message(self, msg: StanzaBase):
 | 
			
		||||
        self.xmpp.event("message_reply", msg)
 | 
			
		||||
 | 
			
		||||
    def send_reply(self, reply_to: JidStr, reply_id: str, **msg_kwargs):
 | 
			
		||||
    def make_reply(self, reply_to: JidStr, reply_id: str,
 | 
			
		||||
                   fallback: Optional[str] = None,
 | 
			
		||||
                   quoted_nick: Optional[str] = None, **msg_kwargs):
 | 
			
		||||
        """Create a replies message stanza
 | 
			
		||||
 | 
			
		||||
        :param reply_to: Full JID of the quoted author
 | 
			
		||||
        :param reply_id: ID of the message to reply to
 | 
			
		||||
        :param fallback: Body of the quoted message
 | 
			
		||||
        :param quoted_nick: nickname of the quoted participant
 | 
			
		||||
        :param msg_kwargs: Parameters are consistent with the make_message method,
 | 
			
		||||
            required parameters are ``mto`` and ``mbody``
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        msg = self.xmpp.make_message(**msg_kwargs)
 | 
			
		||||
        msg["reply"]["to"] = reply_to
 | 
			
		||||
        msg["reply"]["id"] = reply_id
 | 
			
		||||
        if fallback:
 | 
			
		||||
            msg["reply"].add_quoted_fallback(fallback, quoted_nick)
 | 
			
		||||
        return msg
 | 
			
		||||
 | 
			
		||||
    def send_reply(self, reply_to: JidStr, reply_id: str,
 | 
			
		||||
                   fallback: Optional[str] = None,
 | 
			
		||||
                   quoted_nick: Optional[str] = None, **msg_kwargs):
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        :param reply_to: Full JID of the quoted author
 | 
			
		||||
        :param reply_id: ID of the message to reply to
 | 
			
		||||
        :param fallback: Body of the quoted message
 | 
			
		||||
        :param quoted_nick: nickname of the quoted participant
 | 
			
		||||
        """
 | 
			
		||||
        msg = self.xmpp.make_message(**msg_kwargs)
 | 
			
		||||
        msg["reply"]["to"] = reply_to
 | 
			
		||||
        msg["reply"]["id"] = reply_id
 | 
			
		||||
        msg = self.make_reply(reply_to, reply_id, fallback, quoted_nick, **msg_kwargs)
 | 
			
		||||
        msg.send()
 | 
			
		||||
 
 | 
			
		||||
@@ -30,11 +30,11 @@ class Reply(ElementBase):
 | 
			
		||||
        if nickname:
 | 
			
		||||
            quoted = "> " + nickname + ":\n" + quoted
 | 
			
		||||
        msg["body"] = quoted + msg["body"]
 | 
			
		||||
        fallback = Fallback()
 | 
			
		||||
        fallback["for"] = NS
 | 
			
		||||
        fallback["body"]["start"] = 0
 | 
			
		||||
        fallback["body"]["end"] = len(quoted)
 | 
			
		||||
        msg.append(fallback)
 | 
			
		||||
        fallback_elem = Fallback()
 | 
			
		||||
        fallback_elem["for"] = NS
 | 
			
		||||
        fallback_elem["body"]["start"] = 0
 | 
			
		||||
        fallback_elem["body"]["end"] = len(quoted)
 | 
			
		||||
        msg.append(fallback_elem)
 | 
			
		||||
 | 
			
		||||
    def get_fallback_body(self) -> str:
 | 
			
		||||
        msg = self.parent()
 | 
			
		||||
@@ -50,6 +50,23 @@ class Reply(ElementBase):
 | 
			
		||||
            return body[start:end]
 | 
			
		||||
        else:
 | 
			
		||||
            return ""
 | 
			
		||||
        
 | 
			
		||||
    def strip_fallback_content(self) -> str:
 | 
			
		||||
        msg = self.parent() 
 | 
			
		||||
        for fallback in msg["fallbacks"]:
 | 
			
		||||
            if fallback["for"] == NS:
 | 
			
		||||
                break
 | 
			
		||||
        else:
 | 
			
		||||
            return msg["body"]
 | 
			
		||||
 | 
			
		||||
        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():
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								slixmpp/plugins/xep_0490/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								slixmpp/plugins/xep_0490/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
from slixmpp.plugins.base import register_plugin
 | 
			
		||||
 | 
			
		||||
from . import stanza
 | 
			
		||||
from .mds import XEP_0490
 | 
			
		||||
 | 
			
		||||
register_plugin(XEP_0490)
 | 
			
		||||
 | 
			
		||||
__all__ = ['stanza', 'XEP_0490']
 | 
			
		||||
							
								
								
									
										42
									
								
								slixmpp/plugins/xep_0490/mds.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								slixmpp/plugins/xep_0490/mds.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
from asyncio import Future
 | 
			
		||||
 | 
			
		||||
from slixmpp import Iq
 | 
			
		||||
from slixmpp.plugins import BasePlugin
 | 
			
		||||
from slixmpp.types import JidStr
 | 
			
		||||
 | 
			
		||||
from . import stanza
 | 
			
		||||
from ..xep_0004 import Form
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XEP_0490(BasePlugin):
 | 
			
		||||
    """
 | 
			
		||||
    XEP-0490: Message Displayed Synchronization
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    name = "xep_0490"
 | 
			
		||||
    description = "XEP-0490: Message Displayed Synchronization"
 | 
			
		||||
    dependencies = {"xep_0060", "xep_0163", "xep_0223", "xep_0359"}
 | 
			
		||||
    stanza = stanza
 | 
			
		||||
 | 
			
		||||
    def plugin_init(self):
 | 
			
		||||
        stanza.register_plugin()
 | 
			
		||||
        self.xmpp.plugin["xep_0163"].register_pep(
 | 
			
		||||
            "message_displayed_synchronization",
 | 
			
		||||
            stanza.Displayed,
 | 
			
		||||
        )
 | 
			
		||||
        self.xmpp.plugin["xep_0223"].node_profiles[self.stanza.NS] = {
 | 
			
		||||
            "pubsub#max_items": "max",
 | 
			
		||||
            "pubsub#send_last_published_item": "never",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def flag_chat(self, chat: JidStr, stanza_id: str, **kwargs) -> Future[Iq]:
 | 
			
		||||
        displayed = stanza.Displayed()
 | 
			
		||||
        displayed["stanza_id"]["id"] = stanza_id
 | 
			
		||||
        return self.xmpp.plugin["xep_0223"].store(
 | 
			
		||||
            displayed, node=stanza.NS, id=str(chat), **kwargs
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def catch_up(self, **kwargs):
 | 
			
		||||
        return self.xmpp.plugin["xep_0060"].get_items(
 | 
			
		||||
            self.xmpp.boundjid.bare, stanza.NS, **kwargs
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										17
									
								
								slixmpp/plugins/xep_0490/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								slixmpp/plugins/xep_0490/stanza.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
from slixmpp import register_stanza_plugin
 | 
			
		||||
from slixmpp.plugins.xep_0060.stanza import Item
 | 
			
		||||
from slixmpp.xmlstream import ElementBase
 | 
			
		||||
from slixmpp.plugins.xep_0359.stanza import StanzaID
 | 
			
		||||
 | 
			
		||||
NS = "urn:xmpp:mds:displayed:0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Displayed(ElementBase):
 | 
			
		||||
    namespace = NS
 | 
			
		||||
    name = "displayed"
 | 
			
		||||
    plugin_attrib = "displayed"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_plugin():
 | 
			
		||||
    register_stanza_plugin(Displayed, StanzaID)
 | 
			
		||||
    register_stanza_plugin(Item, Displayed)
 | 
			
		||||
							
								
								
									
										13
									
								
								slixmpp/plugins/xep_0492/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								slixmpp/plugins/xep_0492/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
# 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"]
 | 
			
		||||
							
								
								
									
										21
									
								
								slixmpp/plugins/xep_0492/notify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								slixmpp/plugins/xep_0492/notify.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
# 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()
 | 
			
		||||
							
								
								
									
										106
									
								
								slixmpp/plugins/xep_0492/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								slixmpp/plugins/xep_0492/stanza.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
# 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)
 | 
			
		||||
@@ -103,6 +103,7 @@ from slixmpp.plugins.xep_0437 import XEP_0437
 | 
			
		||||
from slixmpp.plugins.xep_0439 import XEP_0439
 | 
			
		||||
from slixmpp.plugins.xep_0444 import XEP_0444
 | 
			
		||||
from slixmpp.plugins.xep_0461 import XEP_0461
 | 
			
		||||
from slixmpp.plugins.xep_0490 import XEP_0490
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PluginsDict(TypedDict):
 | 
			
		||||
@@ -199,3 +200,4 @@ class PluginsDict(TypedDict):
 | 
			
		||||
    xep_0439: XEP_0439
 | 
			
		||||
    xep_0444: XEP_0444
 | 
			
		||||
    xep_0461: XEP_0461
 | 
			
		||||
    xep_0490: XEP_0490
 | 
			
		||||
 
 | 
			
		||||
@@ -53,17 +53,20 @@ MucAffiliation = Literal[
 | 
			
		||||
    'outcast', 'member', 'admin', 'owner', 'none'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
OptJid = Optional[JID]
 | 
			
		||||
JidStr = Union[str, JID]
 | 
			
		||||
OptJidStr = Optional[Union[str, JID]]
 | 
			
		||||
 | 
			
		||||
class PresenceArgs(TypedDict, total=False):
 | 
			
		||||
    pfrom: JID
 | 
			
		||||
    pto: JID
 | 
			
		||||
    pfrom: JidStr
 | 
			
		||||
    pto: JidStr
 | 
			
		||||
    pshow: PresenceShows
 | 
			
		||||
    ptype: PresenceTypes
 | 
			
		||||
    pstatus: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MucRoomItem(TypedDict, total=False):
 | 
			
		||||
    jid: JID
 | 
			
		||||
    jid: str
 | 
			
		||||
    role: MucRole
 | 
			
		||||
    affiliation: MucAffiliation
 | 
			
		||||
    show: Optional[PresenceShows]
 | 
			
		||||
@@ -75,10 +78,6 @@ MucRoomItemKeys = Literal[
 | 
			
		||||
    'jid', 'role', 'affiliation', 'show', 'status',  'alt_nick',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
OptJid = Optional[JID]
 | 
			
		||||
JidStr = Union[str, JID]
 | 
			
		||||
OptJidStr = Optional[Union[str, JID]]
 | 
			
		||||
 | 
			
		||||
MAMDefault = Literal['always', 'never', 'roster']
 | 
			
		||||
 | 
			
		||||
FilterString = Literal['in', 'out', 'out_sync']
 | 
			
		||||
@@ -98,6 +97,7 @@ ErrorConditions = Literal[
 | 
			
		||||
    "not-allowed",
 | 
			
		||||
    "not-authorized",
 | 
			
		||||
    "payment-required",
 | 
			
		||||
    "policy-violation",
 | 
			
		||||
    "recipient-unavailable",
 | 
			
		||||
    "redirect",
 | 
			
		||||
    "registration-required",
 | 
			
		||||
@@ -110,8 +110,21 @@ ErrorConditions = Literal[
 | 
			
		||||
    "unexpected-request",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# https://xmpp.org/registrar/disco-categories.html#client
 | 
			
		||||
ClientTypes = Literal[
 | 
			
		||||
    "bot",
 | 
			
		||||
    "console",
 | 
			
		||||
    "game",
 | 
			
		||||
    "handheld",
 | 
			
		||||
    "pc",
 | 
			
		||||
    "phone",
 | 
			
		||||
    "sms",
 | 
			
		||||
    "tablet",
 | 
			
		||||
    "web",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault',
 | 
			
		||||
    'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
 | 
			
		||||
    'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes'
 | 
			
		||||
    'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes', 'ClientTypes'
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@
 | 
			
		||||
# We don't want to have to import the entire library
 | 
			
		||||
# just to get the version info for setup.py
 | 
			
		||||
 | 
			
		||||
__version__ = '1.8.5'
 | 
			
		||||
__version_info__ = (1, 8, 5)
 | 
			
		||||
__version__ = '1.8.6'
 | 
			
		||||
__version_info__ = (1, 8, 6)
 | 
			
		||||
 
 | 
			
		||||
@@ -732,6 +732,9 @@ class ElementBase(object):
 | 
			
		||||
                return plugin[full_attrib]
 | 
			
		||||
            return plugin
 | 
			
		||||
        else:
 | 
			
		||||
            # XXX: This is legacy from SleekXMPP
 | 
			
		||||
            #      We've probably missed the opportunity to fix it
 | 
			
		||||
            logging.warning("Unknown stanza interface: %s" % full_attrib)
 | 
			
		||||
            return ''
 | 
			
		||||
 | 
			
		||||
    def __setitem__(self, attrib: str, value: Any) -> Any:
 | 
			
		||||
@@ -1230,7 +1233,7 @@ class ElementBase(object):
 | 
			
		||||
            if type(item) == XML_TYPE:
 | 
			
		||||
                return self.appendxml(item)
 | 
			
		||||
            else:
 | 
			
		||||
                raise TypeError
 | 
			
		||||
                raise TypeError(f"Cannot append {item!r} to a stanza")
 | 
			
		||||
        self.xml.append(item.xml)
 | 
			
		||||
        if item.__class__ == self.plugin_tag_map.get(item.tag_name(), None):
 | 
			
		||||
            self.init_plugin(item.plugin_attrib,
 | 
			
		||||
 
 | 
			
		||||
@@ -281,7 +281,8 @@ class XMLStream(asyncio.BaseProtocol):
 | 
			
		||||
    __slow_tasks: List[Task]
 | 
			
		||||
    __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.socket = None
 | 
			
		||||
        self._connect_loop_wait = 0
 | 
			
		||||
@@ -298,9 +299,12 @@ class XMLStream(asyncio.BaseProtocol):
 | 
			
		||||
        # A dict of {name: handle}
 | 
			
		||||
        self.scheduled_events = {}
 | 
			
		||||
 | 
			
		||||
        self.ssl_context = ssl.create_default_context()
 | 
			
		||||
        self.ssl_context.check_hostname = True
 | 
			
		||||
        self.ssl_context.verify_mode = ssl.CERT_REQUIRED
 | 
			
		||||
        if ssl_context is None:
 | 
			
		||||
            self.ssl_context = ssl.create_default_context()
 | 
			
		||||
            self.ssl_context.check_hostname = True
 | 
			
		||||
            self.ssl_context.verify_mode = ssl.CERT_REQUIRED
 | 
			
		||||
        else:
 | 
			
		||||
            self.ssl_context = ssl_context
 | 
			
		||||
 | 
			
		||||
        self.event_when_connected = "connected"
 | 
			
		||||
 | 
			
		||||
@@ -1350,6 +1354,7 @@ class XMLStream(asyncio.BaseProtocol):
 | 
			
		||||
            if isinstance(data, (RootStanza, str)) and not passthrough:
 | 
			
		||||
                self.__queued_stanzas.append((data, use_filters))
 | 
			
		||||
                log.debug('NOT SENT: %s %s', type(data), data)
 | 
			
		||||
                self.event('stanza_not_sent', data)
 | 
			
		||||
                return
 | 
			
		||||
        self.waiting_queue.put_nowait((data, use_filters))
 | 
			
		||||
 | 
			
		||||
@@ -1413,7 +1418,11 @@ class XMLStream(asyncio.BaseProtocol):
 | 
			
		||||
 | 
			
		||||
        # Convert the raw XML object into a stanza object. If no registered
 | 
			
		||||
        # stanza type applies, a generic StanzaBase stanza will be used.
 | 
			
		||||
        stanza: Optional[StanzaBase] = self._build_stanza(xml)
 | 
			
		||||
        try:
 | 
			
		||||
            stanza: Optional[StanzaBase] = self._build_stanza(xml)
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            log.exception("Unable to parse stanza: %s,\n%s", exc, xml)
 | 
			
		||||
            stanza = None
 | 
			
		||||
        for filter in self.__filters['in']:
 | 
			
		||||
            if stanza is not None:
 | 
			
		||||
                filter = cast(SyncFilter, filter)
 | 
			
		||||
 
 | 
			
		||||
@@ -95,6 +95,21 @@ class TestDataForms(SlixTest):
 | 
			
		||||
          </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):
 | 
			
		||||
        """Testing setting form values"""
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +132,7 @@ class TestDataForms(SlixTest):
 | 
			
		||||
                <value>b</value>
 | 
			
		||||
              </field>
 | 
			
		||||
            </x>
 | 
			
		||||
          </message>""")
 | 
			
		||||
          </message>""", use_values=False)
 | 
			
		||||
 | 
			
		||||
    def testSubmitType(self):
 | 
			
		||||
        """Test that setting type to 'submit' clears extra details"""
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ class TestJabberSearch(SlixTest):
 | 
			
		||||
            ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
 | 
			
		||||
        )
 | 
			
		||||
        iq["search"]["form"].add_field(var="x-gender", value="male")
 | 
			
		||||
        iq["id"] = "0"
 | 
			
		||||
        self.check(
 | 
			
		||||
            iq,
 | 
			
		||||
            """
 | 
			
		||||
 
 | 
			
		||||
@@ -2,38 +2,33 @@ import unittest
 | 
			
		||||
from slixmpp import Message
 | 
			
		||||
from slixmpp.test import SlixTest
 | 
			
		||||
from slixmpp.plugins.xep_0424 import stanza
 | 
			
		||||
from slixmpp.plugins.xep_0422 import stanza as astanza
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRetraction(SlixTest):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        astanza.register_plugins()
 | 
			
		||||
        stanza.register_plugins()
 | 
			
		||||
 | 
			
		||||
    def testRetract(self):
 | 
			
		||||
        message = Message()
 | 
			
		||||
        message['apply_to']['id'] = 'some-id'
 | 
			
		||||
        message['apply_to']['retract']
 | 
			
		||||
        message['retract']['id'] = 'some-id'
 | 
			
		||||
 | 
			
		||||
        self.check(message, """
 | 
			
		||||
<message>
 | 
			
		||||
  <apply-to xmlns="urn:xmpp:fasten:0" id="some-id">
 | 
			
		||||
      <retract xmlns="urn:xmpp:message-retract:0"/>
 | 
			
		||||
  </apply-to>
 | 
			
		||||
  <retract xmlns="urn:xmpp:message-retract:1" id="some-id"/>
 | 
			
		||||
</message>
 | 
			
		||||
        """, use_values=False)
 | 
			
		||||
 | 
			
		||||
    def testRetracted(self):
 | 
			
		||||
        message = Message()
 | 
			
		||||
        message['retracted']['stamp'] = '2019-09-20T23:09:32Z'
 | 
			
		||||
        message['retracted']['origin_id']['id'] = 'originid'
 | 
			
		||||
        message['retracted']['id'] = 'originid'
 | 
			
		||||
 | 
			
		||||
        self.check(message, """
 | 
			
		||||
<message>
 | 
			
		||||
  <retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0">
 | 
			
		||||
    <origin-id xmlns="urn:xmpp:sid:0" id="originid"/>
 | 
			
		||||
  </retracted>
 | 
			
		||||
  <retracted stamp="2019-09-20T23:09:32Z"
 | 
			
		||||
             xmlns="urn:xmpp:message-retract:1"
 | 
			
		||||
             id="originid" /> 
 | 
			
		||||
</message>
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +1,48 @@
 | 
			
		||||
import unittest
 | 
			
		||||
from slixmpp import Message, Iq, JID
 | 
			
		||||
from slixmpp.test import SlixTest
 | 
			
		||||
from slixmpp.plugins.xep_0424 import stanza as stanza424
 | 
			
		||||
from slixmpp.plugins.xep_0425 import stanza
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestModeration(SlixTest):
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        stanza424.register_plugins()
 | 
			
		||||
        stanza.register_plugins()
 | 
			
		||||
 | 
			
		||||
    def testModerate(self):
 | 
			
		||||
        iq = Iq()
 | 
			
		||||
        iq['type'] = 'set'
 | 
			
		||||
        iq['id'] = 'a'
 | 
			
		||||
        iq['apply_to']['id'] = 'some-id'
 | 
			
		||||
        iq['apply_to']['moderate'].enable('retract')
 | 
			
		||||
        iq['apply_to']['moderate']['reason'] = 'R'
 | 
			
		||||
        iq['moderate']['id'] = 'some-id'
 | 
			
		||||
        iq['moderate'].enable('retract')
 | 
			
		||||
        iq['moderate']['reason'] = 'R'
 | 
			
		||||
 | 
			
		||||
        self.check(iq, """
 | 
			
		||||
<iq type='set' id='a'>
 | 
			
		||||
  <apply-to id="some-id" xmlns="urn:xmpp:fasten:0">
 | 
			
		||||
    <moderate xmlns='urn:xmpp:message-moderate:0'>
 | 
			
		||||
      <retract xmlns='urn:xmpp:message-retract:0'/>
 | 
			
		||||
      <reason>R</reason>
 | 
			
		||||
    </moderate>
 | 
			
		||||
  </apply-to>
 | 
			
		||||
  <moderate xmlns='urn:xmpp:message-moderate:1' id='some-id'>
 | 
			
		||||
    <retract xmlns='urn:xmpp:message-retract:1'/>
 | 
			
		||||
    <reason>R</reason>
 | 
			
		||||
  </moderate>
 | 
			
		||||
</iq>
 | 
			
		||||
        """, use_values=False)
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
    def testModerated(self):
 | 
			
		||||
        message = Message()
 | 
			
		||||
        message['moderated']['by'] = JID('toto@titi')
 | 
			
		||||
        message['moderated']['retracted']['stamp'] = '2019-09-20T23:09:32Z'
 | 
			
		||||
        message['moderated']['reason'] = 'R'
 | 
			
		||||
        message['retract']['id'] = 'some-id'
 | 
			
		||||
        message['retract']['moderated']['by'] = JID('toto@titi')
 | 
			
		||||
        message['retract']['moderated']['occupant-id']['id'] = 'oc-id'
 | 
			
		||||
        message['retract']['reason'] = 'R'
 | 
			
		||||
 | 
			
		||||
        self.check(message, """
 | 
			
		||||
<message>
 | 
			
		||||
  <moderated xmlns="urn:xmpp:message-moderate:0" by="toto@titi">
 | 
			
		||||
    <retracted stamp="2019-09-20T23:09:32Z" xmlns="urn:xmpp:message-retract:0" />
 | 
			
		||||
  <retract id='some-id' xmlns='urn:xmpp:message-retract:1'>
 | 
			
		||||
    <moderated by='toto@titi' xmlns='urn:xmpp:message-moderate:1'>
 | 
			
		||||
      <occupant-id xmlns="urn:xmpp:occupant-id:0" id="oc-id" />
 | 
			
		||||
    </moderated>
 | 
			
		||||
    <reason>R</reason>
 | 
			
		||||
  </moderated>
 | 
			
		||||
  </retract>
 | 
			
		||||
</message>
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										101
									
								
								tests/test_stanza_xep_0446.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								tests/test_stanza_xep_0446.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										178
									
								
								tests/test_stanza_xep_0492.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								tests/test_stanza_xep_0492.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
# 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)
 | 
			
		||||
@@ -8,12 +8,14 @@ class TestReply(SlixTest):
 | 
			
		||||
        self.stream_start(plugins=["xep_0461"])
 | 
			
		||||
 | 
			
		||||
    def testFallBackBody(self):
 | 
			
		||||
        async def on_reply(msg):
 | 
			
		||||
        def on_reply(msg):
 | 
			
		||||
            start = msg["fallback"]["body"]["start"]
 | 
			
		||||
            end = msg["fallback"]["body"]["end"]
 | 
			
		||||
            self.xmpp["xep_0461"].send_reply(
 | 
			
		||||
                reply_to=msg.get_from(),
 | 
			
		||||
                reply_id=msg.get_id(),
 | 
			
		||||
                fallback=msg["reply"].strip_fallback_content(),
 | 
			
		||||
                quoted_nick="res",
 | 
			
		||||
                mto="test@test.com",
 | 
			
		||||
                mbody=f"{start} to {end}",
 | 
			
		||||
            )
 | 
			
		||||
@@ -26,7 +28,7 @@ class TestReply(SlixTest):
 | 
			
		||||
              <reply xmlns="urn:xmpp:reply:0" id="some-id" />
 | 
			
		||||
              <body>> quoted\nsome-body</body>
 | 
			
		||||
                <fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
 | 
			
		||||
                   <body start="0" end="8" />
 | 
			
		||||
                   <body start="0" end="9" />
 | 
			
		||||
                </fallback>
 | 
			
		||||
            </message>
 | 
			
		||||
            """
 | 
			
		||||
@@ -34,8 +36,11 @@ class TestReply(SlixTest):
 | 
			
		||||
        self.send(
 | 
			
		||||
            """
 | 
			
		||||
            <message xmlns="jabber:client" to="test@test.com" type="normal">
 | 
			
		||||
              <body>> res:\n> some-body\n0 to 9</body>
 | 
			
		||||
              <reply xmlns="urn:xmpp:reply:0" id="other-id" to="from@from.com/res" />
 | 
			
		||||
              <body>0 to 8</body>
 | 
			
		||||
              <fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'>
 | 
			
		||||
                <body start="0" end="19" />
 | 
			
		||||
              </fallback>
 | 
			
		||||
            </message>
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										135
									
								
								tests/test_stream_xep_0490.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								tests/test_stream_xep_0490.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
import unittest.mock
 | 
			
		||||
 | 
			
		||||
from slixmpp.test import SlixTest
 | 
			
		||||
# from slixmpp.plugins import xep_0490
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestMessageDisplaySynchronization(SlixTest):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.stream_start(jid="juliet@capulet.lit", plugins={"xep_0490"})
 | 
			
		||||
 | 
			
		||||
    def test_catch_up(self):
 | 
			
		||||
        future = self.xmpp.plugin["xep_0490"].catch_up()
 | 
			
		||||
        self.send(  # language=XML
 | 
			
		||||
            """
 | 
			
		||||
            <iq type="get" to="juliet@capulet.lit" id="1">
 | 
			
		||||
              <pubsub xmlns="http://jabber.org/protocol/pubsub">
 | 
			
		||||
                <items node="urn:xmpp:mds:displayed:0" />
 | 
			
		||||
              </pubsub>
 | 
			
		||||
            </iq>
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
        self.recv(  # language=XML
 | 
			
		||||
            """
 | 
			
		||||
            <iq type='result'
 | 
			
		||||
                to='juliet@capulet.lit/balcony'
 | 
			
		||||
                id='1'>
 | 
			
		||||
              <pubsub xmlns='http://jabber.org/protocol/pubsub'>
 | 
			
		||||
                <items node='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                  <item id='romeo@montegue.lit'>
 | 
			
		||||
                    <displayed xmlns='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                      <stanza-id xmlns='urn:xmpp:sid:0'
 | 
			
		||||
                                 id='0f710f2b-52ed-4d52-b928-784dad74a52b'
 | 
			
		||||
                                 by='juliet@capulet.lit'/>
 | 
			
		||||
                    </displayed>
 | 
			
		||||
                  </item>
 | 
			
		||||
                  <item id='example@conference.shakespeare.lit'>
 | 
			
		||||
                    <displayed xmlns='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                      <stanza-id xmlns='urn:xmpp:sid:0'
 | 
			
		||||
                                 id='ca21deaf-812c-48f1-8f16-339a674f2864'
 | 
			
		||||
                                 by='example@conference.shakespeare.lit'/>
 | 
			
		||||
                    </displayed>
 | 
			
		||||
                  </item>
 | 
			
		||||
                </items>
 | 
			
		||||
              </pubsub>
 | 
			
		||||
            </iq>
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
        iq = future.result()
 | 
			
		||||
        item = list(iq["pubsub"]["items"])
 | 
			
		||||
        self.assertEqual(item[0]["id"], "romeo@montegue.lit")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            item[0]["displayed"]["stanza_id"]["id"],
 | 
			
		||||
            "0f710f2b-52ed-4d52-b928-784dad74a52b",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item[1]["id"], "example@conference.shakespeare.lit")
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            item[1]["displayed"]["stanza_id"]["id"],
 | 
			
		||||
            "ca21deaf-812c-48f1-8f16-339a674f2864",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_flag_chat(self):
 | 
			
		||||
        self.xmpp.plugin["xep_0490"].flag_chat(
 | 
			
		||||
            "romeo@montegue.lit", "0f710f2b-52ed-4d52-b928-784dad74a52b"
 | 
			
		||||
        )
 | 
			
		||||
        self.send(  # language=XML
 | 
			
		||||
            """
 | 
			
		||||
            <iq type='set' id='1'>
 | 
			
		||||
              <pubsub xmlns='http://jabber.org/protocol/pubsub'>
 | 
			
		||||
                <publish node='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                  <item id='romeo@montegue.lit'>
 | 
			
		||||
                    <displayed xmlns='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                      <stanza-id xmlns='urn:xmpp:sid:0' 
 | 
			
		||||
                                 id="0f710f2b-52ed-4d52-b928-784dad74a52b" />
 | 
			
		||||
                    </displayed>
 | 
			
		||||
                  </item>
 | 
			
		||||
                </publish>
 | 
			
		||||
                <publish-options>
 | 
			
		||||
                  <x xmlns='jabber:x:data' type='submit'>
 | 
			
		||||
                    <field var='FORM_TYPE' type='hidden'>
 | 
			
		||||
                      <value>http://jabber.org/protocol/pubsub#publish-options</value>
 | 
			
		||||
                    </field>
 | 
			
		||||
                    <field var='pubsub#persist_items'>
 | 
			
		||||
                      <value>1</value>
 | 
			
		||||
                    </field>
 | 
			
		||||
                    <field var='pubsub#max_items'>
 | 
			
		||||
                      <value>max</value>
 | 
			
		||||
                    </field>
 | 
			
		||||
                    <field var='pubsub#send_last_published_item'>
 | 
			
		||||
                      <value>never</value>
 | 
			
		||||
                    </field>
 | 
			
		||||
                    <field var='pubsub#access_model'>
 | 
			
		||||
                      <value>whitelist</value>
 | 
			
		||||
                    </field>
 | 
			
		||||
                  </x>
 | 
			
		||||
                </publish-options>
 | 
			
		||||
              </pubsub>
 | 
			
		||||
            </iq>
 | 
			
		||||
            """,
 | 
			
		||||
            use_values=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_notification(self):
 | 
			
		||||
        handler = unittest.mock.Mock()
 | 
			
		||||
 | 
			
		||||
        self.xmpp.add_event_handler(
 | 
			
		||||
            "message_displayed_synchronization_publish", handler
 | 
			
		||||
        )
 | 
			
		||||
        self.recv(  # language=XML
 | 
			
		||||
            """
 | 
			
		||||
            <message from='juliet@capulet.lit' to='juliet@capulet.lit/balcony' type='headline' id='new-displayed-pep-event'>
 | 
			
		||||
              <event xmlns='http://jabber.org/protocol/pubsub#event'>
 | 
			
		||||
                <items node='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                  <item id='romeo@montegue.lit'>
 | 
			
		||||
                    <displayed xmlns='urn:xmpp:mds:displayed:0'>
 | 
			
		||||
                      <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='0423e3a9-d516-493d-bb06-bee0e51ab9fb'/>
 | 
			
		||||
                    </displayed>
 | 
			
		||||
                  </item>
 | 
			
		||||
                </items>
 | 
			
		||||
              </event>
 | 
			
		||||
            </message>
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
        handler.assert_called()
 | 
			
		||||
        msg = handler.call_args[0][0]
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            msg["pubsub_event"]["items"]["item"]["id"], "romeo@montegue.lit"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"],
 | 
			
		||||
            "0423e3a9-d516-493d-bb06-bee0e51ab9fb",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageDisplaySynchronization)
 | 
			
		||||
		Reference in New Issue
	
	Block a user