Compare commits
	
		
			188 Commits
		
	
	
		
			slix-1.8.0
			...
			test-ci
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | dcaf812a28 | ||
|   | ae4de043d2 | ||
|   | 998bbb80ad | ||
|   | 5a5b36ab39 | ||
|   | f151f0a7ab | ||
|   | 2424a3b36f | ||
|   | 1c4bbbce8e | ||
|   | 66d552d057 | ||
|   | b8205a9ae4 | ||
|   | 85b7210115 | ||
|   | 909c865524 | ||
|   | 586d2f5107 | ||
|   | 9f7260747f | ||
|   | c41209510a | ||
|   | 9266486f46 | ||
|   | 5226858e0c | ||
|   | 7128ea249b | ||
|   | 992d80dd09 | ||
|   | c25305e80f | ||
|   | 6765f84133 | ||
|   | 31fe7f7e06 | ||
|   | 84a7ac020f | ||
|   | 331c1c1e21 | ||
|   | 28a60c22e2 | ||
|   | af934b5bdf | ||
|   | 897f876504 | ||
|   | 2888be17ab | ||
|   | 975e31229c | ||
|   | 6e9e66139d | ||
|   | 380ac04d52 | ||
|   | 9e5b530607 | ||
|   | 71de274fab | ||
|   | 5a0b02378d | ||
|   | 9fc82e9e6f | ||
|   | ca90d3908e | ||
|   | 7de5cbcf33 | ||
|   | 76a11d4899 | ||
|   | dcfa0f20f9 | ||
|   | 7732af8991 | ||
|   | 25c28ff5d1 | ||
|   | e3e0d8f43e | ||
|   | 13729e47a6 | ||
|   | f12860bfad | ||
|   | bcbc7281e7 | ||
|   | 8787aa1064 | ||
|   | f3522eb84b | ||
|   | da9646cdaa | ||
|   | db1fc5fbc5 | ||
|   | 209554e63f | ||
|   | 2d02ef9bcb | ||
|   | 18c3db4d6e | ||
|   | 6d6fdc6419 | ||
|   | 4936fb06bf | ||
|   | 5e47286445 | ||
|   | 8bead23799 | ||
|   | 56c906f207 | ||
|   | 876c82037f | ||
|   | fae4a38e84 | ||
|   | 2b59d299a1 | ||
|   | 51a4efb0f4 | ||
|   | 8f77bd4ee5 | ||
|   | 71128349a4 | ||
|   | bc2cebae6c | ||
|   | 2080d08d63 | ||
|   | e16f72d32d | ||
|   | 4fa068da54 | ||
|   | 21e5cd4435 | ||
|   | 1a40699bcc | ||
|   | ebb8bd1e71 | ||
|   | 78b42bdbbe | ||
|   | abd3f40e96 | ||
|   | b6f148e4e6 | ||
|   | 968fb0bac3 | ||
|   | 8dcbcbf8a0 | ||
|   | de7b2d33a3 | ||
|   | fd1af054c5 | ||
|   | e34fbfb28f | ||
|   | af16832ad0 | ||
|   | 40a857de65 | ||
|   | 79ffa1668f | ||
|   | b4b1efe058 | ||
|   | de358464d0 | ||
|   | 92b4f2a7eb | ||
|   | 1f934d375c | ||
|   | 700ce6b32e | ||
|   | 5efa9804ba | ||
|   | 9b0be1ca2b | ||
|   | 5c19f16287 | ||
|   | af07864cbb | ||
|   | dc4b1c7367 | ||
|   | 4a6064772c | ||
|   | 80a89061f1 | ||
|   | 8f4d8f76d1 | ||
|   | 656248ede7 | ||
|   | 980afe791f | ||
|   | 3725177d0b | ||
|   | 26fb0d1f91 | ||
|   | 5eb17e7633 | ||
|   | fdca7d82c4 | ||
|   | 9b89401b36 | ||
|   | 7300f1285e | ||
|   | 9b51be1e17 | ||
|   | 89b1e1e682 | ||
|   | a7501abe56 | ||
|   | 6940e4276b | ||
|   | 752f4258df | ||
|   | b60b1b985d | ||
|   | e93e43df66 | ||
|   | cfd1af88eb | ||
|   | 65636b8cce | ||
|   | 7a0fb97083 | ||
|   | 189bbcce19 | ||
|   | 79607e43f1 | ||
|   | e062181f84 | ||
|   | 97b0c7ffac | ||
|   | c2ece57dee | ||
|   | afdfa1ee57 | ||
|   | cba5dc7ddc | ||
|   | b3a6c7a4ea | ||
|   | 11e27d1d7d | ||
|   | fbdff30dda | ||
|   | 62701bc562 | ||
|   | b14918808c | ||
|   | f5cb9fe66b | ||
|   | 8bd53f7098 | ||
|   | c955cf1c66 | ||
|   | 6904ae63f5 | ||
|   | 1caada197a | ||
|   | 450aaa7f86 | ||
|   | d43c83800e | ||
|   | 14786abd34 | ||
|   | 1f47acaec1 | ||
|   | ed820bf551 | ||
|   | afedfa4b06 | ||
|   | 5998069203 | ||
|   | 356f16f5af | ||
|   | b8f301b26f | ||
|   | ffaeb31219 | ||
|   | 9560f39de7 | ||
|   | f7a38a028a | ||
|   | 65d70fe417 | ||
|   | 108a256537 | ||
|   | 78a5f79240 | ||
|   | fc63768cfc | ||
|   | 90e79af18a | ||
|   | 5e5a741994 | ||
|   | b44ab17c8f | ||
|   | afb5419b68 | ||
|   | a1a5f3984d | ||
|   | 8eb8769862 | ||
|   | 5ceb48bbcd | ||
|   | 916894ab7c | ||
|   | 2b45c22fcb | ||
|   | 566e7dc771 | ||
|   | aa492f905c | ||
|   | e1a240ec6c | ||
|   | 771839242c | ||
|   | 8bac744009 | ||
|   | 88d2f5dae4 | ||
|   | f7902d056e | ||
|   | 41afbb10df | ||
|   | aca4addb9c | ||
|   | 914ce40fd5 | ||
|   | 82ff68cfac | ||
|   | 28d44ecf74 | ||
|   | fcec6742cf | ||
|   | fedbb248ec | ||
|   | e8679fe32b | ||
|   | 06e4e480c1 | ||
|   | 82ee250295 | ||
|   | 53d38a8115 | ||
|   | 41d733e77f | ||
|   | 0fba8fd7f8 | ||
|   | b899baabd8 | ||
|   | acad41f3b7 | ||
|   | bde5aaaf3e | ||
|   | 7222ade0dd | ||
|   | 14a6c7801d | ||
|   | b52540e49f | ||
|   | c1aeab328b | ||
|   | 51644e301b | ||
|   | bc8af3cc61 | ||
|   | 3c08f471cf | ||
|   | 54b724c28b | ||
|   | abd699593f | ||
|   | 4202ed4cd5 | ||
|   | 60df4ef7aa | ||
|   | ad610c7ded | 
| @@ -12,15 +12,15 @@ mypy: | ||||
|     - pip3 install mypy | ||||
|     - mypy slixmpp | ||||
|  | ||||
| test: | ||||
| test-3.7: | ||||
|   stage: test | ||||
|   tags: | ||||
|     - docker | ||||
|   image: ubuntu:latest | ||||
|   image: python:3.7 | ||||
|   script: | ||||
|     - apt update | ||||
|     - apt install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp | ||||
|     - apt-get update | ||||
|     - apt-get install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp cryptography | ||||
|     - ./run_tests.py | ||||
|  | ||||
| test-3.10: | ||||
| @@ -30,34 +30,45 @@ test-3.10: | ||||
|   image: python:3.10 | ||||
|   script: | ||||
|     - apt update | ||||
|     - apt install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp | ||||
|     - 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-rc | ||||
|   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 update | ||||
|     - apt install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp | ||||
|     - 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: ubuntu:latest | ||||
|   image: python:3 | ||||
|   only: | ||||
|     variables: | ||||
|         - $CI_ACCOUNT1 | ||||
|         - $CI_ACCOUNT2 | ||||
|   script: | ||||
|     - apt update | ||||
|     - apt install -y python3 python3-pip cython3 gpg | ||||
|     - apt-get update | ||||
|     - apt-get install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp aiodns | ||||
|     - ./run_integration_tests.py | ||||
|  | ||||
|   | ||||
							
								
								
									
										22
									
								
								.readthedocs.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.readthedocs.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # .readthedocs.yaml | ||||
| # Read the Docs configuration file | ||||
| # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details | ||||
|  | ||||
| # Required | ||||
| version: 2 | ||||
|  | ||||
| # Set the version of Python and other tools you might need | ||||
| build: | ||||
|   os: ubuntu-22.04 | ||||
|   tools: | ||||
|     python: "3.11" | ||||
|  | ||||
| # Build documentation in the docs/ directory with Sphinx | ||||
| sphinx: | ||||
|   configuration: docs/conf.py | ||||
|  | ||||
| # We recommend specifying your dependencies to enable reproducible builds: | ||||
| # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html | ||||
| python: | ||||
|    install: | ||||
|    - requirements: docs/requirements.txt | ||||
							
								
								
									
										6
									
								
								.woodpecker/lint.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.woodpecker/lint.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| steps: | ||||
|   mypy: | ||||
|     image: python:3 | ||||
|     commands: | ||||
|       - pip3 install mypy types-setuptools | ||||
|       - mypy slixmpp | ||||
							
								
								
									
										10
									
								
								.woodpecker/test-integration.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.woodpecker/test-integration.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| steps: | ||||
|   test_integration: | ||||
|     image: "python:3.11" | ||||
|     secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password, ci_muc_server] | ||||
|     commands: | ||||
|       - apt-get update | ||||
|       - apt-get install -y python3-pip cython3 gpg idn libidn-dev | ||||
|       - pip3 install emoji aiohttp aiodns | ||||
|       - python3 setup.py build_ext --inplace | ||||
|       - ./run_integration_tests.py | ||||
							
								
								
									
										17
									
								
								.woodpecker/test.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.woodpecker/test.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| steps: | ||||
|   unit_tests: | ||||
|     image: "python:${TAG}" | ||||
|     commands: | ||||
|     - apt-get update | ||||
|     - apt-get install -y python3 python3-pip cython3 gpg | ||||
|     - pip3 install emoji aiohttp cryptography | ||||
|     - ./run_tests.py | ||||
|  | ||||
| matrix: | ||||
|   TAG: | ||||
|     - "3.7" | ||||
|     - "3.9" | ||||
|     - "3.8" | ||||
|     - "3.10" | ||||
|     - "3.11" | ||||
|     - "3.12" | ||||
| @@ -5,7 +5,7 @@ To contribute, the preferred way is to commit your changes on some | ||||
| publicly-available git repository (on a fork `on github | ||||
| <https://github.com/poezio/slixmpp>`_ or on your own repository) and to | ||||
| notify the developers with either: | ||||
|  - a ticket `on the bug tracker <https://lab.louiz.org/poezio/slixmpp/issues/new>`_ | ||||
|  - a ticket `on the bug tracker <https://codeberg.org/poezio/slixmpp/issues/new>`_ | ||||
|  - a pull request on github | ||||
|  - a simple message on `the XMPP MUC <xmpp:slixmpp@muc.poez.io>`_ | ||||
|  | ||||
|   | ||||
							
								
								
									
										74
									
								
								doap.xml
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								doap.xml
									
									
									
									
									
								
							| @@ -8,13 +8,13 @@ | ||||
|     <shortdesc xml:lang="en">Elegant Python library for XMPP</shortdesc> | ||||
|     <shortdesc xml:lang="fr">Bibliothèque pour XMPP élégante, en Python</shortdesc> | ||||
|  | ||||
|     <homepage rdf:resource="https://lab.louiz.org/poezio/slixmpp/"/> | ||||
|     <download-page rdf:resource="https://lab.louiz.org/poezio/slixmpp/tags"/> | ||||
|     <bug-database rdf:resource="https://lab.louiz.org/poezio/slixmpp/issues"/> | ||||
|     <homepage rdf:resource="https://codeberg.org/poezio/slixmpp/"/> | ||||
|     <download-page rdf:resource="https://codeberg.org/poezio/slixmpp/tags"/> | ||||
|     <bug-database rdf:resource="https://codeberg.org/poezio/slixmpp/issues"/> | ||||
|     <developer-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/> | ||||
|     <support-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/> | ||||
|  | ||||
|     <license rdf:resource="https://lab.louiz.org/poezio/slixmpp/blob/master/LICENSE"/> | ||||
|     <license rdf:resource="https://codeberg.org/poezio/slixmpp/raw/brach/master/LICENSE"/> | ||||
|  | ||||
|     <language>en</language> | ||||
|  | ||||
| @@ -59,8 +59,8 @@ | ||||
|  | ||||
|     <repository> | ||||
|         <GitRepository> | ||||
|             <browse rdf:resource="https://lab.louiz.org/poezio/slixmpp"/> | ||||
|             <location rdf:resource="https://lab.louiz.org/poezio/slixmpp.git"/> | ||||
|             <browse rdf:resource="https://codeberg.org/poezio/slixmpp"/> | ||||
|             <location rdf:resource="https://codeberg.org/poezio/slixmpp.git"/> | ||||
|         </GitRepository> | ||||
|     </repository> | ||||
|  | ||||
| @@ -455,6 +455,14 @@ | ||||
|             <xmpp:since>1.0</xmpp:since> | ||||
|         </xmpp:SupportedXep> | ||||
|     </implements> | ||||
|     <implements> | ||||
|         <xmpp:SupportedXep> | ||||
|             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0175.html"/> | ||||
|             <xmpp:status>complete</xmpp:status> | ||||
|             <xmpp:version>1.2</xmpp:version> | ||||
|             <xmpp:since>1.0</xmpp:since> | ||||
|         </xmpp:SupportedXep> | ||||
|     </implements> | ||||
|     <implements> | ||||
|         <xmpp:SupportedXep> | ||||
|             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/> | ||||
| @@ -776,7 +784,7 @@ | ||||
|         <xmpp:SupportedXep> | ||||
|             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0377.html"/> | ||||
|             <xmpp:status>complete</xmpp:status> | ||||
|             <xmpp:version>0.2</xmpp:version> | ||||
|             <xmpp:version>0.3</xmpp:version> | ||||
|             <xmpp:since>1.6.0</xmpp:since> | ||||
|         </xmpp:SupportedXep> | ||||
|     </implements> | ||||
| @@ -892,6 +900,15 @@ | ||||
|             <xmpp:since>1.6.0</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:SupportedXep> | ||||
|     </implements> | ||||
|  | ||||
|     <release> | ||||
|         <Version> | ||||
| @@ -995,28 +1012,63 @@ | ||||
|         <Version> | ||||
|             <revision>1.6.0</revision> | ||||
|             <created>2020-12-12</created> | ||||
|             <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.6.0/slixmpp-slix-1.6.0.tar.gz"/> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.6.0.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.7.0</revision> | ||||
|             <created>2021-01-29</created> | ||||
|             <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.0/slixmpp-slix-1.7.0.tar.gz"/> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.0.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.7.1</revision> | ||||
|             <created>2021-04-30</created> | ||||
|             <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.1/slixmpp-slix-1.7.1.tar.gz"/> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.7.1.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.0</revision> | ||||
|             <created>2022-02-27</created> | ||||
|             <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.0/slixmpp-slix-1.8.0.tar.gz"/> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.0.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.1</revision> | ||||
|             <created>2022-03-20</created> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.1.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.2</revision> | ||||
|             <created>2022-04-06</created> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.2.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.3</revision> | ||||
|             <created>2022-11-12</created> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.3.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.4</revision> | ||||
|             <created>2023-05-28</created> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.4.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
|     <release> | ||||
|         <Version> | ||||
|             <revision>1.8.5</revision> | ||||
|             <created>2024-02-02</created> | ||||
|             <file-release rdf:resource="https://codeberg.org/poezio/slixmpp/archive/slix-1.8.5.tar.gz"/> | ||||
|         </Version> | ||||
|     </release> | ||||
| </Project> | ||||
|   | ||||
							
								
								
									
										18
									
								
								docs/api/plugins/xep_0055.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/api/plugins/xep_0055.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
|  | ||||
| XEP-0055: Jabber search | ||||
| ======================= | ||||
|  | ||||
| .. module:: slixmpp.plugins.xep_0055 | ||||
|  | ||||
| .. autoclass:: XEP_0055 | ||||
|     :members: | ||||
|     :exclude-members: session_bind, plugin_init, plugin_end | ||||
|  | ||||
|  | ||||
| Stanza elements | ||||
| --------------- | ||||
|  | ||||
| .. automodule:: slixmpp.plugins.xep_0055.stanza | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|  | ||||
							
								
								
									
										17
									
								
								docs/api/plugins/xep_0292.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docs/api/plugins/xep_0292.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
|  | ||||
| XEP-0292: vCard4 Over XMPP | ||||
| ========================== | ||||
|  | ||||
| .. module:: slixmpp.plugins.xep_0292 | ||||
|  | ||||
| .. autoclass:: XEP_0292 | ||||
|     :members: | ||||
|     :exclude-members: plugin_init, plugin_end | ||||
|  | ||||
|  | ||||
| Stanza elements | ||||
| --------------- | ||||
|  | ||||
| .. automodule:: slixmpp.plugins.xep_0292.stanza | ||||
|     :members: | ||||
|     :undoc-members: | ||||
| @@ -11,7 +11,7 @@ Create and Run a Server Component | ||||
|     <xmpp:slixmpp@muc.poez.io?join>`_. | ||||
|  | ||||
| If you have not yet installed Slixmpp, do so now by either checking out a version | ||||
| with `Git <https://lab.louiz.org/poezio/slixmpp>`_. | ||||
| with `Git <https://codeberg.org/poezio/slixmpp>`_. | ||||
|  | ||||
| Many XMPP applications eventually graduate to requiring to run as a server | ||||
| component in order to meet scalability requirements. To demonstrate how to | ||||
|   | ||||
| @@ -11,7 +11,7 @@ Slixmpp Quickstart - Echo Bot | ||||
|     <xmpp:slixmpp@muc.poez.io?join>`_. | ||||
|  | ||||
| If you have not yet installed Slixmpp, do so now by either checking out a version | ||||
| with `Git <https://lab.louiz.org/poezio/slixmpp>`_. | ||||
| with `Git <https://codeberg.org/poezio/slixmpp>`_. | ||||
|  | ||||
| As a basic starting project, we will create an echo bot which will reply to any | ||||
| messages sent to it. We will also go through adding some basic command line configuration | ||||
| @@ -325,7 +325,7 @@ The Final Product | ||||
| ----------------- | ||||
|  | ||||
| Here then is what the final result should look like after working through the guide above. The code | ||||
| can also be found in the Slixmpp `examples directory <https://lab.louiz.org/poezio/slixmpp/tree/master/examples>`_. | ||||
| can also be found in the Slixmpp `examples directory <https://codeberg.org/poezio/slixmpp/src/branch/master/examples>`_. | ||||
|  | ||||
| .. compound:: | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ Multi-User Chat (MUC) Bot | ||||
|     <xmpp:slixmpp@muc.poez.io?join>`_. | ||||
|  | ||||
| If you have not yet installed Slixmpp, do so now by either checking out a version | ||||
| from `Git <https://lab.louiz.org/poezio/slixmpp>`_. | ||||
| from `Git <https://codeberg.org/poezio/slixmpp>`_. | ||||
|  | ||||
| Now that you've got the basic gist of using Slixmpp by following the | ||||
| echobot example (:ref:`echobot`), we can use one of the bundled plugins | ||||
|   | ||||
| @@ -4,9 +4,9 @@ Slixmpp | ||||
| .. sidebar:: Get the Code | ||||
|  | ||||
|     The latest source code for Slixmpp may be found on the `Git repo | ||||
|     <https://lab.louiz.org/poezio/slixmpp>`_. :: | ||||
|     <https://codeberg.org/poezio/slixmpp>`_. :: | ||||
|  | ||||
|         git clone https://lab.louiz.org/poezio/slixmpp | ||||
|         git clone https://codeberg.org/poezio/slixmpp | ||||
|  | ||||
|     An XMPP chat room is available for discussing and getting help with slixmpp. | ||||
|  | ||||
| @@ -14,7 +14,7 @@ Slixmpp | ||||
|         `slixmpp@muc.poez.io <xmpp:slixmpp@muc.poez.io?join>`_ | ||||
|  | ||||
|     **Reporting bugs** | ||||
|         You can report bugs at http://lab.louiz.org/poezio/slixmpp/issues. | ||||
|         You can report bugs at http://codeberg.org/poezio/slixmpp/issues. | ||||
|  | ||||
| Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.7+, | ||||
|  | ||||
|   | ||||
							
								
								
									
										95
									
								
								docs/projects.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								docs/projects.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| Projects Using Slixmpp | ||||
| ====================== | ||||
|  | ||||
| Applications | ||||
| ------------ | ||||
|  | ||||
| sendxmpp-py | ||||
| ~~~~~~~~~~~ | ||||
| sendxmpp is a command line program and is the XMPP equivalent of sendmail. It is a Python version of the original sendxmpp which is written in Perl. | ||||
|  | ||||
| - `Source <https://github.com/moparisthebest/sendxmpp-py>`_ | ||||
|  | ||||
| Bots | ||||
| ---- | ||||
|  | ||||
| BotLogMauve | ||||
| ~~~~~~~~~~~ | ||||
| XMPP bot which logs groupchat messages. Logs are in text format, with one file per day and per groupchat. | ||||
|  | ||||
| - `Source <https://git.khaganat.net/khaganat/BotLogMauve>`_ | ||||
|  | ||||
| LinkBot | ||||
| ~~~~~~~ | ||||
| This bot reveals the title of any shared link in a groupchat for quick content insight. | ||||
|  | ||||
| - `Source <https://git.xmpp-it.net/mario/XMPPBot>`_ | ||||
|  | ||||
| llama-bot | ||||
| ~~~~~~~~~ | ||||
| Llama-bot enables engaging communication with the LLM (large language model) of llama.cpp, providing seamless and dynamic conversation with it. | ||||
|  | ||||
| - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_ | ||||
| - `Source <https://github.com/decent-im/llama-bot>`_ | ||||
| - `Demo <xmpp:llama@decent.im?message>`_ | ||||
|  | ||||
| Morbot | ||||
| ~~~~~~ | ||||
| Morbot is a simple Slixmpp bot that will take new articles from listed RSS feeds and send them to assigned XMPP MUCs. | ||||
|  | ||||
| - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_ | ||||
| - `Source <https://codeberg.org/TheCoffeMaker/Morbot>`_ | ||||
|  | ||||
| Slixfeed | ||||
| ~~~~~~~~ | ||||
| Slixfeed aims to be an easy to use and fully-featured news aggregator bot for XMPP. It provides a convenient access to Blogs, Fediverse and News websites along with filtering functionality. | ||||
|  | ||||
| - `Groupchat <xmpp:slixfeed@chat.woodpeckersnest.space?join>`_ | ||||
| - `Source <https://gitgud.io/sjehuda/slixfeed>`_ | ||||
|  | ||||
| sms4you | ||||
| ~~~~~~~ | ||||
| sms4you forwards messages from and to SMS and connects either with sms4you-xmpp or sms4you-email to choose the other mean of communication. Nice for receiving or sending SMS, independently from carrying a SIM card. | ||||
|  | ||||
| - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_ | ||||
| - `Homepage <https://sms4you-team.pages.debian.net/sms4you/>`_ | ||||
| - `Source <https://salsa.debian.org/sms4you-team/sms4you>`_ | ||||
|  | ||||
| Stable Diffusion | ||||
| ~~~~~~~~~~~~~~~~ | ||||
| XMPP bot that generates digital images from textual descriptions. | ||||
|  | ||||
| - `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_ | ||||
| - `Source <https://www.nicoco.fr/blog/2022/08/31/xmpp-bot-stable-diffusion/>`_ | ||||
|  | ||||
| WhisperBot | ||||
| ~~~~~~~~~~ | ||||
| XMPP bot that transliterates audio messages using OpenAI's Whisper libraries. | ||||
|  | ||||
| - `Groupchat <xmpp:slixmpp@muc.poez.io?join>`_ | ||||
| - `Source <https://codeberg.org/TheCoffeMaker/WhisperBot>`_ | ||||
|  | ||||
| XMPP MUC Message Gateway | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| A multipurpose JSON forwarder microservice from HTTP POST to XMPP MUC room over TLSv1.2 with SliXMPP. | ||||
|  | ||||
| - `Source <https://github.com/immanuelfodor/xmpp-muc-message-gateway>`_ | ||||
|  | ||||
| Services | ||||
| -------- | ||||
|  | ||||
| AtomToPubsub | ||||
| ~~~~~~~~~~~~ | ||||
| AtomToPubsub is a simple Python script that parses Atom + RSS feeds and pushes the entries to a designated XMPP Pubsub Node. | ||||
|  | ||||
| - `Groupchat <xmpp:movim@conference.movim.eu?join>`_ | ||||
| - `Source <https://github.com/imattau/atomtopubsub>`_ | ||||
|  | ||||
| Slidge | ||||
| ~~~~~~ | ||||
|  | ||||
| Slidge is a general purpose XMPP gateway framework in Python. | ||||
|  | ||||
| - `Groupchat <xmpp:slidge@conference.nicoco.fr?join>`_ | ||||
| - `Homepage <https://slidge.im/core/>`_ | ||||
| - `Source <https://sr.ht/~nicoco/slidge>`_ | ||||
| @@ -5,11 +5,16 @@ | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
|  | ||||
| from typing import Optional | ||||
|  | ||||
| import sys | ||||
| import logging | ||||
| from pathlib import Path | ||||
| from getpass import getpass | ||||
| from argparse import ArgumentParser | ||||
|  | ||||
| import slixmpp | ||||
| from slixmpp import JID | ||||
| from slixmpp.exceptions import IqTimeout | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
| @@ -21,20 +26,40 @@ class HttpUpload(slixmpp.ClientXMPP): | ||||
|     A basic client asking an entity if they confirm the access to an HTTP URL. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, jid, password, recipient, filename, domain=None): | ||||
|     def __init__( | ||||
|         self, | ||||
|         jid: JID, | ||||
|         password: str, | ||||
|         recipient: JID, | ||||
|         filename: Path, | ||||
|         domain: Optional[JID] = None, | ||||
|         encrypted: bool = False, | ||||
|     ): | ||||
|         slixmpp.ClientXMPP.__init__(self, jid, password) | ||||
|  | ||||
|         self.recipient = recipient | ||||
|         self.filename = filename | ||||
|         self.domain = domain | ||||
|         self.encrypted = encrypted | ||||
|  | ||||
|         self.add_event_handler("session_start", self.start) | ||||
|  | ||||
|     async def start(self, event): | ||||
|         log.info('Uploading file %s...', self.filename) | ||||
|         try: | ||||
|             url = await self['xep_0363'].upload_file( | ||||
|                 self.filename, domain=self.domain, timeout=10 | ||||
|             upload_file = self['xep_0363'].upload_file | ||||
|             if self.encrypted and not self['xep_0454']: | ||||
|                 print( | ||||
|                     'The xep_0454 module isn\'t available. ' | ||||
|                     'Ensure you have \'cryptography\' ' | ||||
|                     'from extras_require installed.', | ||||
|                     file=sys.stderr, | ||||
|                 ) | ||||
|                 return | ||||
|             elif self.encrypted: | ||||
|                 upload_file = self['xep_0454'].upload_file | ||||
|             url = await upload_file( | ||||
|                 self.filename, domain=self.domain, timeout=10, | ||||
|             ) | ||||
|         except IqTimeout: | ||||
|             raise TimeoutError('Could not send message in time') | ||||
| @@ -79,6 +104,10 @@ if __name__ == '__main__': | ||||
|     parser.add_argument("--domain", | ||||
|                         help="Domain to use for HTTP File Upload (leave out for your own server’s)") | ||||
|  | ||||
|     parser.add_argument("-e", "--encrypt", dest="encrypted", | ||||
|                         help="Whether to encrypt", action="store_true", | ||||
|                         default=False) | ||||
|  | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # Setup logging. | ||||
| @@ -86,15 +115,41 @@ if __name__ == '__main__': | ||||
|                         format='%(levelname)-8s %(message)s') | ||||
|  | ||||
|     if args.jid is None: | ||||
|         args.jid = input("Username: ") | ||||
|         args.jid = JID(input("Username: ")) | ||||
|     if args.password is None: | ||||
|         args.password = getpass("Password: ") | ||||
|  | ||||
|     xmpp = HttpUpload(args.jid, args.password, args.recipient, args.file, args.domain) | ||||
|     domain = args.domain | ||||
|     if domain is not None: | ||||
|         domain = JID(domain) | ||||
|  | ||||
|     if args.encrypted: | ||||
|         print( | ||||
|             'You are using the --encrypt flag. ' | ||||
|             'Be aware that the transport being used is NOT end-to-end ' | ||||
|             'encrypted. The server will be able to decrypt the file.', | ||||
|             file=sys.stderr, | ||||
|         ) | ||||
|  | ||||
|     xmpp = HttpUpload( | ||||
|         jid=args.jid, | ||||
|         password=args.password, | ||||
|         recipient=JID(args.recipient), | ||||
|         filename=Path(args.file), | ||||
|         domain=domain, | ||||
|         encrypted=args.encrypted, | ||||
|     ) | ||||
|     xmpp.register_plugin('xep_0066') | ||||
|     xmpp.register_plugin('xep_0071') | ||||
|     xmpp.register_plugin('xep_0128') | ||||
|     xmpp.register_plugin('xep_0363') | ||||
|     try: | ||||
|         xmpp.register_plugin('xep_0454') | ||||
|     except slixmpp.plugins.base.PluginNotFound: | ||||
|         log.error( | ||||
|             'Could not load xep_0454. ' | ||||
|             'Ensure you have \'cryptography\' from extras_require installed.' | ||||
|         ) | ||||
|  | ||||
|     # Connect to the XMPP server and start processing XMPP stanzas. | ||||
|     xmpp.connect() | ||||
|   | ||||
| @@ -10,7 +10,7 @@ UNIQUE = uuid4().hex | ||||
| class TestMUC(SlixIntegration): | ||||
|  | ||||
|     async def asyncSetUp(self): | ||||
|         self.mucserver = self.envjid('CI_MUC_SERVER') | ||||
|         self.mucserver = self.envjid('CI_MUC_SERVER', default='chat.jabberfr.org') | ||||
|         self.muc = JID('%s@%s' % (UNIQUE, self.mucserver)) | ||||
|         self.add_client( | ||||
|             self.envjid('CI_ACCOUNT1'), | ||||
|   | ||||
							
								
								
									
										10
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								setup.py
									
									
									
									
									
								
							| @@ -80,16 +80,22 @@ setup( | ||||
|     long_description=LONG_DESCRIPTION, | ||||
|     author='Florent Le Coz', | ||||
|     author_email='louiz@louiz.org', | ||||
|     url='https://lab.louiz.org/poezio/slixmpp', | ||||
|     url='https://codeberg.org/poezio/slixmpp', | ||||
|     license='MIT', | ||||
|     platforms=['any'], | ||||
|     package_data={'slixmpp': ['py.typed']}, | ||||
|     packages=packages, | ||||
|     ext_modules=ext_modules, | ||||
|     install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'typing_extensions; python_version < "3.8.0"'], | ||||
|     install_requires=[ | ||||
|         'aiodns>=1.0', | ||||
|         'pyasn1', | ||||
|         'pyasn1_modules', | ||||
|         'typing_extensions; python_version < "3.8.0"', | ||||
|     ], | ||||
|     extras_require={ | ||||
|         'XEP-0363': ['aiohttp'], | ||||
|         'XEP-0444 compliance': ['emoji'], | ||||
|         'XEP-0454': ['cryptography'], | ||||
|         'Safer XML parsing': ['defusedxml'], | ||||
|     }, | ||||
|     classifiers=CLASSIFIERS, | ||||
|   | ||||
| @@ -4,14 +4,17 @@ | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| import logging | ||||
| logging.getLogger(__name__).addHandler(logging.NullHandler()) | ||||
| from os import getenv | ||||
|  | ||||
| # Use defusedxml if available | ||||
| try: | ||||
|     import defusedxml | ||||
|     defusedxml.defuse_stdlib() | ||||
| except ImportError: | ||||
|     pass | ||||
| # Use defusedxml if wanted | ||||
| # Since enabling it can have adverse consequences for the programs using | ||||
| # slixmpp, do not enable it by default. | ||||
| if  getenv('SLIXMPP_ENABLE_DEFUSEDXML', default='false').lower() == 'true': | ||||
|     try: | ||||
|         import defusedxml | ||||
|         defusedxml.defuse_stdlib() | ||||
|     except ImportError: | ||||
|         pass | ||||
|  | ||||
| from slixmpp.stanza import Message, Presence, Iq | ||||
| from slixmpp.jid import JID, InvalidJID | ||||
|   | ||||
| @@ -140,7 +140,7 @@ class BaseXMPP(XMLStream): | ||||
|         self.use_presence_ids = True | ||||
|  | ||||
|         #: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas. | ||||
|         self.use_origin_id = True | ||||
|         self.use_origin_id = False | ||||
|  | ||||
|         #: The API registry is a way to process callbacks based on | ||||
|         #: JID+node combinations. Each callback in the registry is | ||||
| @@ -279,13 +279,13 @@ class BaseXMPP(XMLStream): | ||||
|         if self.plugin_whitelist: | ||||
|             plugin_list = self.plugin_whitelist | ||||
|         else: | ||||
|             plugin_list = plugins.__all__ | ||||
|             plugin_list = plugins.PLUGINS | ||||
|  | ||||
|         for plugin in plugin_list: | ||||
|             if plugin in plugins.__all__: | ||||
|             if plugin in plugins.PLUGINS: | ||||
|                 self.register_plugin(plugin) | ||||
|             else: | ||||
|                 raise NameError("Plugin %s not in plugins.__all__." % plugin) | ||||
|                 raise NameError("Plugin %s not in plugins.PLUGINS." % plugin) | ||||
|  | ||||
|     def __getitem__(self, key): | ||||
|         """Return a plugin given its name, if it has been registered.""" | ||||
|   | ||||
| @@ -138,8 +138,8 @@ class ClientXMPP(BaseXMPP): | ||||
|         self.credentials['password'] = value | ||||
|  | ||||
|     def connect(self, address: Optional[Tuple[str, int]] = None,  # type: ignore | ||||
|                 use_ssl: bool = False, force_starttls: bool = True, | ||||
|                 disable_starttls: bool = False) -> None: | ||||
|                 use_ssl: Optional[bool] = None, force_starttls: Optional[bool] = None, | ||||
|                 disable_starttls: Optional[bool] = None) -> None: | ||||
|         """Connect to the XMPP server. | ||||
|  | ||||
|         When no address is given, a SRV lookup for the server will | ||||
| @@ -166,8 +166,8 @@ class ClientXMPP(BaseXMPP): | ||||
|             host, port = (self.boundjid.host, 5222) | ||||
|             self.dns_service = 'xmpp-client' | ||||
|  | ||||
|         return XMLStream.connect(self, host, port, use_ssl=use_ssl, | ||||
|                                  force_starttls=force_starttls, disable_starttls=disable_starttls) | ||||
|         XMLStream.connect(self, host, port, use_ssl=use_ssl, | ||||
|                           force_starttls=force_starttls, disable_starttls=disable_starttls) | ||||
|  | ||||
|     def register_feature(self, name: str, handler: Callable, restart: bool = False, order: int = 5000) -> None: | ||||
|         """Register a stream feature handler. | ||||
|   | ||||
| @@ -9,13 +9,16 @@ | ||||
| import logging | ||||
| import hashlib | ||||
|  | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import Message, Iq, Presence | ||||
| from slixmpp.basexmpp import BaseXMPP | ||||
| from slixmpp.stanza import Handshake | ||||
| from slixmpp.stanza.error import Error | ||||
| from slixmpp.xmlstream import XMLStream | ||||
| from slixmpp.xmlstream import ET | ||||
| from slixmpp.xmlstream.matcher import MatchXPath | ||||
| from slixmpp.xmlstream.handler import Callback | ||||
|  | ||||
| from slixmpp.xmlstream.stanzabase import register_stanza_plugin | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -39,9 +42,17 @@ class ComponentXMPP(BaseXMPP): | ||||
|                       should be used instead of the standard | ||||
|                       ``'jabber:component:accept'`` namespace. | ||||
|                       Defaults to ``False``. | ||||
|     :param fix_error_ns: Fix the namespace of error stanzas. | ||||
|         If you use ``use_jc_ns`` namespace, you probably want that, but | ||||
|         it can be a problem if you use both a ClientXMPP and a ComponentXMPP | ||||
|         in the same interpreter. This is ``False`` by default for backwards | ||||
|         compatibility. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False): | ||||
|     def __init__(self, jid, secret, | ||||
|                  host=None, port=None, plugin_config=None, | ||||
|                  plugin_whitelist=None, use_jc_ns=False, | ||||
|                  fix_error_ns=False): | ||||
|  | ||||
|         if not plugin_whitelist: | ||||
|             plugin_whitelist = [] | ||||
| @@ -53,6 +64,8 @@ class ComponentXMPP(BaseXMPP): | ||||
|         else: | ||||
|             default_ns = 'jabber:component:accept' | ||||
|         BaseXMPP.__init__(self, jid, default_ns) | ||||
|         if fix_error_ns: | ||||
|             self._fix_error_ns() | ||||
|  | ||||
|         self.auto_authorize = None | ||||
|         self.stream_header = '<stream:stream %s %s to="%s">' % ( | ||||
| @@ -77,7 +90,14 @@ class ComponentXMPP(BaseXMPP): | ||||
|         self.add_event_handler('presence_probe', | ||||
|                                self._handle_probe) | ||||
|  | ||||
|     def connect(self, host=None, port=None, use_ssl=False): | ||||
|     def _fix_error_ns(self): | ||||
|         Error.namespace = self.default_ns | ||||
|         for st in Message, Iq, Presence: | ||||
|             register_stanza_plugin(st, Error) | ||||
|  | ||||
|     def connect(self, host: Optional[str] = None, port: int = 0, use_ssl: Optional[bool] = None, | ||||
|                 force_starttls: Optional[bool] = None, | ||||
|                 disable_starttls: Optional[bool] = None) -> None: | ||||
|         """Connect to the server. | ||||
|  | ||||
|  | ||||
| @@ -87,17 +107,18 @@ class ComponentXMPP(BaseXMPP): | ||||
|                      Defauts to :attr:`server_port`. | ||||
|         :param use_ssl: Flag indicating if SSL should be used by connecting | ||||
|                         directly to a port using SSL. | ||||
|         :param force_starttls: UNUSED | ||||
|         :param disable_starttls: UNUSED | ||||
|         """ | ||||
|         if host is None: | ||||
|             host = self.server_host | ||||
|         if port is None: | ||||
|             port = self.server_port | ||||
|         if host is not None: | ||||
|             self.server_host = host | ||||
|         if port: | ||||
|             self.server_port = port | ||||
|  | ||||
|         self.server_name = self.boundjid.host | ||||
|  | ||||
|         log.debug("Connecting to %s:%s", host, port) | ||||
|         return XMLStream.connect(self, host=host, port=port, | ||||
|                                        use_ssl=use_ssl) | ||||
|         XMLStream.connect(self, host=self.server_host, port=self.server_port, use_ssl=use_ssl) | ||||
|  | ||||
|     def incoming_filter(self, xml): | ||||
|         """ | ||||
|   | ||||
| @@ -5,6 +5,11 @@ | ||||
| # :copyright: (c) 2011 Nathanael C. Fritz | ||||
| # :license: MIT, see LICENSE for more details | ||||
|  | ||||
| from typing import Dict, Optional | ||||
|  | ||||
| from .types import ErrorConditions, ErrorTypes, JidStr | ||||
|  | ||||
|  | ||||
| class XMPPError(Exception): | ||||
|  | ||||
|     """ | ||||
| @@ -37,12 +42,17 @@ class XMPPError(Exception): | ||||
|                   Defaults to ``True``. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, condition='undefined-condition', text='', | ||||
|                 etype='cancel', extension=None, extension_ns=None, | ||||
|                 extension_args=None, clear=True): | ||||
|     def __init__(self, condition: ErrorConditions='undefined-condition', text='', | ||||
|                 etype: Optional[ErrorTypes]=None, extension=None, extension_ns=None, | ||||
|                 extension_args=None, clear=True, by: Optional[JidStr] = None): | ||||
|         if extension_args is None: | ||||
|             extension_args = {} | ||||
|         if condition not in _DEFAULT_ERROR_TYPES: | ||||
|             raise ValueError("This is not a valid condition type", condition) | ||||
|         if etype is None: | ||||
|             etype = _DEFAULT_ERROR_TYPES[condition] | ||||
|  | ||||
|         self.by = by | ||||
|         self.condition = condition | ||||
|         self.text = text | ||||
|         self.etype = etype | ||||
| @@ -110,3 +120,29 @@ class PresenceError(XMPPError): | ||||
|             etype=pres['error']['type'], | ||||
|         ) | ||||
|         self.presence = pres | ||||
|  | ||||
|  | ||||
| _DEFAULT_ERROR_TYPES: Dict[ErrorConditions, ErrorTypes] = { | ||||
|     "bad-request": "modify", | ||||
|     "conflict": "cancel", | ||||
|     "feature-not-implemented": "cancel", | ||||
|     "forbidden": "auth", | ||||
|     "gone": "modify", | ||||
|     "internal-server-error": "wait", | ||||
|     "item-not-found": "cancel", | ||||
|     "jid-malformed": "modify", | ||||
|     "not-acceptable": "modify", | ||||
|     "not-allowed": "cancel", | ||||
|     "not-authorized": "auth", | ||||
|     "payment-required": "auth", | ||||
|     "recipient-unavailable": "wait", | ||||
|     "redirect": "modify", | ||||
|     "registration-required": "auth", | ||||
|     "remote-server-not-found": "cancel", | ||||
|     "remote-server-timeout": "wait", | ||||
|     "resource-constraint": "wait", | ||||
|     "service-unavailable": "cancel", | ||||
|     "subscription-required": "auth", | ||||
|     "undefined-condition": "cancel", | ||||
|     "unexpected-request": "modify", | ||||
| } | ||||
|   | ||||
| @@ -37,7 +37,8 @@ class FeatureMechanisms(BasePlugin): | ||||
|         'unencrypted_digest': False, | ||||
|         'unencrypted_cram': False, | ||||
|         'unencrypted_scram': True, | ||||
|         'order': 100 | ||||
|         'order': 100, | ||||
|         'tls_version': None, | ||||
|     } | ||||
|  | ||||
|     def plugin_init(self): | ||||
| @@ -96,7 +97,20 @@ class FeatureMechanisms(BasePlugin): | ||||
|                 result[value] = creds.get('email', jid) | ||||
|             elif value == 'channel_binding': | ||||
|                 if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): | ||||
|                     result[value] = self.xmpp.socket.get_channel_binding() | ||||
|                     version = self.xmpp.socket.version() | ||||
|                     # As of now, python does not implement anything else | ||||
|                     # than tls-unique, which is forbidden on TLSv1.3 | ||||
|                     # see https://github.com/python/cpython/issues/95341 | ||||
|                     if version != 'TLSv1.3': | ||||
|                         result[value] = self.xmpp.socket.get_channel_binding( | ||||
|                             cb_type="tls-unique" | ||||
|                         ) | ||||
|                     elif 'tls-exporter' in ssl.CHANNEL_BINDING_TYPES: | ||||
|                         result[value] = self.xmpp.socket.get_channel_binding( | ||||
|                             cb_type="tls-exporter" | ||||
|                         ) | ||||
|                     else: | ||||
|                         result[value] = None | ||||
|                 else: | ||||
|                     result[value] = None | ||||
|             elif value == 'host': | ||||
| @@ -121,6 +135,11 @@ class FeatureMechanisms(BasePlugin): | ||||
|                     result[value] = True | ||||
|                 else: | ||||
|                     result[value] = False | ||||
|             elif value == 'tls_version': | ||||
|                 if isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): | ||||
|                     result[value] = self.xmpp.socket.version() | ||||
|             elif value == 'binding_proposed': | ||||
|                 result[value] = any(x for x in self.mech_list if x.endswith('-PLUS')) | ||||
|             else: | ||||
|                 result[value] = self.config.get(value, False) | ||||
|         return result | ||||
|   | ||||
| @@ -3,8 +3,12 @@ | ||||
| # Copyright (C) 2011  Nathanael C. Fritz | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| from slixmpp.xmlstream import StanzaBase, ElementBase | ||||
| from typing import Set, ClassVar | ||||
| from slixmpp.xmlstream import StanzaBase, ElementBase | ||||
| from slixmpp.xmlstream.xmlstream import InvalidCABundle | ||||
|  | ||||
| import logging | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class STARTTLS(StanzaBase): | ||||
| @@ -36,6 +40,12 @@ class Proceed(StanzaBase): | ||||
|     namespace = 'urn:ietf:params:xml:ns:xmpp-tls' | ||||
|     interfaces: ClassVar[Set[str]] = set() | ||||
|  | ||||
|     def exception(self, e: Exception) -> None: | ||||
|         log.exception('Error handling {%s}%s stanza', | ||||
|                       self.namespace, self.name) | ||||
|         if isinstance(e, InvalidCABundle): | ||||
|             raise e | ||||
|  | ||||
|  | ||||
| class Failure(StanzaBase): | ||||
|     """ | ||||
|   | ||||
| @@ -303,13 +303,15 @@ class JID: | ||||
|  | ||||
|     :param string jid: | ||||
|         A string of the form ``'[user@]domain[/resource]'``. | ||||
|     :param bool bare: | ||||
|         If present, discard the provided resource. | ||||
|  | ||||
|     :raises InvalidJID: | ||||
|     """ | ||||
|  | ||||
|     __slots__ = ('_node', '_domain', '_resource', '_bare', '_full') | ||||
|  | ||||
|     def __init__(self, jid: Optional[Union[str, 'JID']] = None): | ||||
|     def __init__(self, jid: Optional[Union[str, 'JID']] = None, bare: bool = False): | ||||
|         if not jid: | ||||
|             self._node = '' | ||||
|             self._domain = '' | ||||
| @@ -318,11 +320,14 @@ class JID: | ||||
|             self._full = '' | ||||
|             return | ||||
|         elif not isinstance(jid, JID): | ||||
|             self._node, self._domain, self._resource = _parse_jid(jid) | ||||
|             node, domain, resource = _parse_jid(jid) | ||||
|             self._node = node | ||||
|             self._domain = domain | ||||
|             self._resource = resource if not bare else '' | ||||
|         else: | ||||
|             self._node = jid._node | ||||
|             self._domain = jid._domain | ||||
|             self._resource = jid._resource | ||||
|             self._resource = jid._resource if not bare else '' | ||||
|         self._update_bare_full() | ||||
|  | ||||
|     def unescape(self): | ||||
| @@ -368,7 +373,7 @@ class JID: | ||||
|         return self._node | ||||
|  | ||||
|     @node.setter | ||||
|     def node(self, value: str): | ||||
|     def node(self, value: Optional[str]): | ||||
|         self._node = _validate_node(value) | ||||
|         self._update_bare_full() | ||||
|  | ||||
| @@ -386,7 +391,7 @@ class JID: | ||||
|         return self._resource | ||||
|  | ||||
|     @resource.setter | ||||
|     def resource(self, value: str): | ||||
|     def resource(self, value: Optional[str]): | ||||
|         self._resource = _validate_resource(value) | ||||
|         self._update_bare_full() | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
|  | ||||
| # Slixmpp: The Slick XMPP Library | ||||
| # Copyright (C) 2010 Nathanael C. Fritz | ||||
| # This file is part of Slixmpp. | ||||
| @@ -7,7 +6,7 @@ from slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin | ||||
| from slixmpp.plugins.base import register_plugin, load_plugin | ||||
|  | ||||
|  | ||||
| __all__ = [ | ||||
| PLUGINS = [ | ||||
|     # XEPS | ||||
|     'xep_0004',  # Data Forms | ||||
|     'xep_0009',  # Jabber-RPC | ||||
| @@ -24,6 +23,7 @@ __all__ = [ | ||||
|     'xep_0049',  # Private XML Storage | ||||
|     'xep_0050',  # Ad-hoc Commands | ||||
|     'xep_0054',  # vcard-temp | ||||
|     'xep_0055',  # Jabber Search | ||||
|     'xep_0059',  # Result Set Management | ||||
|     'xep_0060',  # Pubsub (Client) | ||||
|     'xep_0065',  # SOCKS5 Bytestreams | ||||
| @@ -76,14 +76,17 @@ __all__ = [ | ||||
|     'xep_0256',  # Last Activity in Presence | ||||
|     'xep_0257',  # Client Certificate Management for SASL EXTERNAL | ||||
|     'xep_0258',  # Security Labels in XMPP | ||||
|     'xep_0264',  # Jingle Content Thumbnails | ||||
| #   'xep_0270',  # XMPP Compliance Suites 2010. Don’t automatically load | ||||
|     'xep_0279',  # Server IP Check | ||||
|     'xep_0280',  # Message Carbons | ||||
|     'xep_0292',  # vCard4 Over XMPP | ||||
|     'xep_0297',  # Stanza Forwarding | ||||
|     'xep_0300',  # Use of Cryptographic Hash Functions in XMPP | ||||
| #   'xep_0302',  # XMPP Compliance Suites 2012. Don’t automatically load | ||||
|     'xep_0308',  # Last Message Correction | ||||
|     'xep_0313',  # Message Archive Management | ||||
|     'xep_0317',  # Hats | ||||
|     'xep_0319',  # Last User Interaction in Presence | ||||
| #   'xep_0323',  # IoT Systems Sensor Data. Don’t automatically load | ||||
| #   'xep_0325',  # IoT Systems Control. Don’t automatically load | ||||
| @@ -93,13 +96,16 @@ __all__ = [ | ||||
|     'xep_0335',  # JSON Containers | ||||
|     'xep_0352',  # Client State Indication | ||||
|     'xep_0353',  # Jingle Message Initiation | ||||
|     'xep_0356',  # Privileged entity | ||||
|     'xep_0359',  # Unique and Stable Stanza IDs | ||||
|     'xep_0363',  # HTTP File Upload | ||||
|     'xep_0369',  # MIX-CORE | ||||
|     'xep_0377',  # Spam reporting | ||||
|     'xep_0380',  # Explicit Message Encryption | ||||
|     'xep_0382',  # Spoiler Messages | ||||
|     'xep_0385',  # Stateless Inline Media Sharing (SIMS) | ||||
|     'xep_0394',  # Message Markup | ||||
|     'xep_0402',  # PEP Native Bookmarks | ||||
|     'xep_0403',  # MIX-Presence | ||||
|     'xep_0404',  # MIX-Anon | ||||
|     'xep_0405',  # MIX-PAM | ||||
| @@ -112,4 +118,16 @@ __all__ = [ | ||||
|     'xep_0439',  # Quick Response | ||||
|     'xep_0441',  # Message Archive Management Preferences | ||||
|     'xep_0444',  # Message Reactions | ||||
|     'xep_0447',  # Stateless file sharing | ||||
|     'xep_0461',  # Message Replies | ||||
|     'xep_0469',  # Bookmarks Pinning | ||||
|     # Meant to be imported by plugins | ||||
| ] | ||||
|  | ||||
| __all__ = PLUGINS + [ | ||||
|     'PluginManager', | ||||
|     'PluginNotFound', | ||||
|     'BasePlugin', | ||||
|     'register_plugin', | ||||
|     'load_plugin', | ||||
| ] | ||||
|   | ||||
| @@ -19,6 +19,8 @@ def _extract_data(data, kind): | ||||
|     stripped = [] | ||||
|     begin_headers = False | ||||
|     begin_data = False | ||||
|     if isinstance(data, bytes): | ||||
|         data = data.decode() | ||||
|     for line in data.split('\n'): | ||||
|         if not begin_headers and 'BEGIN PGP %s' % kind in line: | ||||
|             begin_headers = True | ||||
|   | ||||
| @@ -307,7 +307,7 @@ class XEP_0030(BasePlugin): | ||||
|         return self.api['has_identity'](jid, node, ifrom, data) | ||||
|  | ||||
|     async def get_info_from_domain(self, domain=None, timeout=None, | ||||
|                                    cached=True, callback=None): | ||||
|                                    cached=True, callback=None, **iqkwargs): | ||||
|         """Fetch disco#info of specified domain and one disco#items level below | ||||
|         """ | ||||
|  | ||||
| @@ -315,13 +315,13 @@ class XEP_0030(BasePlugin): | ||||
|             domain = self.xmpp.boundjid.domain | ||||
|  | ||||
|         if not cached or domain not in self.domain_infos: | ||||
|             infos = [self.get_info( | ||||
|                 domain, timeout=timeout)] | ||||
|             infos = [asyncio.create_task(self.get_info( | ||||
|                 domain, timeout=timeout, **iqkwargs))] | ||||
|             iq_items = await self.get_items( | ||||
|                 domain, timeout=timeout) | ||||
|                 domain, timeout=timeout, **iqkwargs) | ||||
|             items = iq_items['disco_items']['items'] | ||||
|             infos += [ | ||||
|                 self.get_info(item[0], timeout=timeout) | ||||
|                 asyncio.create_task(self.get_info(item[0], timeout=timeout, **iqkwargs)) | ||||
|                 for item in items] | ||||
|             info_futures, _ = await asyncio.wait( | ||||
|                 infos, | ||||
| @@ -385,6 +385,8 @@ class XEP_0030(BasePlugin): | ||||
|                 local = True | ||||
|  | ||||
|         ifrom = kwargs.pop('ifrom', None) | ||||
|         if self.xmpp.is_component and ifrom is None: | ||||
|             ifrom = self.xmpp.boundjid | ||||
|         if local: | ||||
|             log.debug("Looking up local disco#info data " | ||||
|                       "for %s, node %s.", jid, node) | ||||
| @@ -455,9 +457,12 @@ class XEP_0030(BasePlugin): | ||||
|                          the XEP-0059 plugin, if the plugin is loaded. | ||||
|                          Otherwise the parameter is ignored. | ||||
|         """ | ||||
|         if ifrom is None and self.xmpp.is_component: | ||||
|             ifrom = self.xmpp.boundjid.bare | ||||
|  | ||||
|         if local or local is None and jid is None: | ||||
|             items = await self.api['get_items'](jid, node, ifrom, kwargs) | ||||
|             return self._wrap(kwargs.get('ifrom', None), jid, items) | ||||
|             return self._wrap(ifrom, jid, items) | ||||
|  | ||||
|         iq = self.xmpp.Iq() | ||||
|         # Check dfrom parameter for backwards compatibility | ||||
|   | ||||
| @@ -323,7 +323,6 @@ class XEP_0045(BasePlugin): | ||||
|  | ||||
|         def add_message(msg: Message): | ||||
|             delay = msg.get_plugin('delay', check=True) | ||||
|             print(delay) | ||||
|             if delay is not None and delay['from'] == room: | ||||
|                 history_buffer.append(msg) | ||||
|  | ||||
| @@ -493,6 +492,8 @@ 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') | ||||
|         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) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| import asyncio | ||||
| import functools | ||||
| import logging | ||||
| import time | ||||
|  | ||||
| @@ -619,8 +620,16 @@ class XEP_0050(BasePlugin): | ||||
|             self.terminate_command(session) | ||||
|  | ||||
|  | ||||
| def _iscoroutine_or_partial_coroutine(handler): | ||||
|     return asyncio.iscoroutinefunction(handler) \ | ||||
|         or isinstance(handler, functools.partial) \ | ||||
|         and asyncio.iscoroutinefunction(handler.func) | ||||
|  | ||||
|  | ||||
| async def _await_if_needed(handler, *args): | ||||
|     if asyncio.iscoroutinefunction(handler): | ||||
|     if handler is None: | ||||
|         raise XMPPError("bad-request", text="The command is completed") | ||||
|     if _iscoroutine_or_partial_coroutine(handler): | ||||
|         log.debug(f"%s is async", handler) | ||||
|         return await handler(*args) | ||||
|     else: | ||||
|   | ||||
| @@ -134,8 +134,10 @@ class XEP_0054(BasePlugin): | ||||
|             return | ||||
|         elif iq['type'] == 'get' and self.xmpp.is_component: | ||||
|             vcard = await self.api['get_vcard'](iq['to'].bare, ifrom=iq['from']) | ||||
|             if isinstance(vcard, Iq): | ||||
|                 vcard.send() | ||||
|             if vcard is None: | ||||
|                 raise XMPPError("item-not-found") | ||||
|             elif isinstance(vcard, Iq): | ||||
|                 await vcard.send() | ||||
|             else: | ||||
|                 iq = iq.reply() | ||||
|                 iq.append(vcard) | ||||
|   | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0055/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0055/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from .search import XEP_0055 | ||||
|  | ||||
|  | ||||
| register_plugin(XEP_0055) | ||||
							
								
								
									
										89
									
								
								slixmpp/plugins/xep_0055/search.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								slixmpp/plugins/xep_0055/search.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| import logging | ||||
|  | ||||
| from slixmpp import CoroutineCallback, StanzaPath, Iq, register_stanza_plugin | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.xmlstream import StanzaBase | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0055(BasePlugin): | ||||
|     """ | ||||
|     XEP-0055: Jabber Search | ||||
|  | ||||
|     The config options are only useful for a "server-side" search feature, | ||||
|     and if the ``provide_search`` option is set to True. | ||||
|  | ||||
|     API | ||||
|     === | ||||
|  | ||||
|     ``search_get_form``: customize the search form content (ie fields) | ||||
|  | ||||
|     ``search_query``: return search results | ||||
|     """ | ||||
|     name = "xep_0055" | ||||
|     description = "XEP-0055: Jabber search" | ||||
|     dependencies = {"xep_0004", "xep_0030"} | ||||
|     stanza = stanza | ||||
|     default_config = { | ||||
|         "form_fields": {"first", "last"}, | ||||
|         "form_instructions": "", | ||||
|         "form_title": "", | ||||
|         "provide_search": True | ||||
|     } | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Iq, stanza.Search) | ||||
|         register_stanza_plugin(stanza.Search, self.xmpp["xep_0004"].stanza.Form) | ||||
|  | ||||
|         if self.provide_search: | ||||
|             self.xmpp["xep_0030"].add_feature(stanza.Search.namespace) | ||||
|             self.xmpp.register_handler( | ||||
|                 CoroutineCallback( | ||||
|                     "search", | ||||
|                     StanzaPath("/iq/search"), | ||||
|                     self._handle_search, | ||||
|                 ) | ||||
|             ) | ||||
|             self.api.register(self._get_form, "search_get_form") | ||||
|             self.api.register(self._get_results, "search_query") | ||||
|  | ||||
|     async def _handle_search(self, iq: StanzaBase): | ||||
|         if iq["search"]["form"].get_values(): | ||||
|             reply = await self.api["search_query"](None, None, iq.get_from(), iq) | ||||
|             reply["search"]["form"]["type"] = "result" | ||||
|         else: | ||||
|             reply = await self.api["search_get_form"](None, None, iq.get_from(), iq) | ||||
|         reply["search"]["form"].add_field( | ||||
|             "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden" | ||||
|         ) | ||||
|         reply.send() | ||||
|  | ||||
|     async def _get_form(self, jid, node, ifrom, iq): | ||||
|         reply = iq.reply() | ||||
|         form = reply["search"]["form"] | ||||
|         form["title"] = self.form_title | ||||
|         form["instructions"] = self.form_instructions | ||||
|         for field in self.form_fields: | ||||
|             form.add_field(field) | ||||
|         return reply | ||||
|  | ||||
|     async def _get_results(self, jid, node, ifrom, iq): | ||||
|         reply = iq.reply() | ||||
|         form = reply["search"]["form"] | ||||
|         form["type"] = "result" | ||||
|  | ||||
|         for field in self.form_fields: | ||||
|             form.add_reported(field) | ||||
|         return reply | ||||
|  | ||||
|     def make_search_iq(self, **kwargs): | ||||
|         iq = self.xmpp.make_iq(itype="set", **kwargs) | ||||
|         iq["search"]["form"].set_type("submit") | ||||
|         iq["search"]["form"].add_field( | ||||
|             "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden" | ||||
|         ) | ||||
|         return iq | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
							
								
								
									
										10
									
								
								slixmpp/plugins/xep_0055/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								slixmpp/plugins/xep_0055/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| from typing import Set, ClassVar | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
|  | ||||
| class Search(ElementBase): | ||||
|     namespace = "jabber:iq:search" | ||||
|     name = "query" | ||||
|     plugin_attrib = "search" | ||||
|     interfaces: ClassVar[Set[str]] = set() | ||||
| @@ -6,7 +6,6 @@ | ||||
| import datetime as dt | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin, register_plugin | ||||
| from slixmpp.thirdparty import tzutc, tzoffset, parse_iso | ||||
|  | ||||
|  | ||||
| # ===================================================================== | ||||
| @@ -21,7 +20,10 @@ def parse(time_str): | ||||
|     Arguments: | ||||
|         time_str -- A formatted timestamp string. | ||||
|     """ | ||||
|     return parse_iso(time_str) | ||||
|     try: | ||||
|         return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f%z') | ||||
|     except ValueError: | ||||
|         return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z') | ||||
|  | ||||
|  | ||||
| def format_date(time_obj): | ||||
| @@ -52,7 +54,7 @@ def format_time(time_obj): | ||||
|     if isinstance(time_obj, dt.datetime): | ||||
|         time_obj = time_obj.timetz() | ||||
|     timestamp = time_obj.isoformat() | ||||
|     if time_obj.tzinfo == tzutc(): | ||||
|     if time_obj.tzinfo == dt.timezone.utc: | ||||
|         timestamp = timestamp[:-6] | ||||
|         return '%sZ' % timestamp | ||||
|     return timestamp | ||||
| @@ -69,7 +71,7 @@ def format_datetime(time_obj): | ||||
|         time_obj -- A datetime object. | ||||
|     """ | ||||
|     timestamp = time_obj.isoformat('T') | ||||
|     if time_obj.tzinfo == tzutc(): | ||||
|     if time_obj.tzinfo == dt.timezone.utc: | ||||
|         timestamp = timestamp[:-6] | ||||
|         return '%sZ' % timestamp | ||||
|     return timestamp | ||||
| @@ -128,9 +130,9 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): | ||||
|     if micro is None: | ||||
|         micro = now.microsecond | ||||
|     if offset in (None, 0): | ||||
|         offset = tzutc() | ||||
|         offset = dt.timezone.utc | ||||
|     elif not isinstance(offset, dt.tzinfo): | ||||
|         offset = tzoffset(None, offset) | ||||
|         offset = dt.timezone(dt.timedelta(seconds=offset)) | ||||
|     value = dt.time(hour, min, sec, micro, offset) | ||||
|     if obj: | ||||
|         return value | ||||
| @@ -175,9 +177,9 @@ def datetime(year=None, month=None, day=None, hour=None, | ||||
|     if micro is None: | ||||
|         micro = now.microsecond | ||||
|     if offset in (None, 0): | ||||
|         offset = tzutc() | ||||
|         offset = dt.timezone.utc | ||||
|     elif not isinstance(offset, dt.tzinfo): | ||||
|         offset = tzoffset(None, offset) | ||||
|         offset = dt.timezone(dt.timedelta(seconds=offset)) | ||||
|  | ||||
|     value = dt.datetime(year, month, day, hour, | ||||
|                        min, sec, micro, offset) | ||||
|   | ||||
| @@ -80,16 +80,16 @@ class Info(ElementBase): | ||||
|         self._set_int('bytes', value) | ||||
|  | ||||
|     def get_height(self) -> int: | ||||
|         self._get_int('height') | ||||
|         return self._get_int('height') | ||||
|  | ||||
|     def set_height(self, value: int): | ||||
|         self._set_int('height', value) | ||||
|  | ||||
|     def get_width(self) -> int: | ||||
|         self._get_int(self, 'width') | ||||
|         return self._get_int('width') | ||||
|  | ||||
|     def set_width(self, value: int): | ||||
|         self._set_int('with', value) | ||||
|         self._set_int('width', value) | ||||
|  | ||||
|  | ||||
| class Pointer(ElementBase): | ||||
|   | ||||
| @@ -7,7 +7,8 @@ import logging | ||||
| import hashlib | ||||
| import base64 | ||||
|  | ||||
| from asyncio import Future | ||||
| from asyncio import Future, Lock | ||||
| from collections import defaultdict | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import __version__ | ||||
| @@ -94,6 +95,9 @@ class XEP_0115(BasePlugin): | ||||
|         disco.assign_verstring = self.assign_verstring | ||||
|         disco.get_verstring = self.get_verstring | ||||
|  | ||||
|         # prevent concurrent fetches for the same hash | ||||
|         self._locks = defaultdict(Lock) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) | ||||
|         self.xmpp.del_filter('out', self._filter_add_caps) | ||||
| @@ -137,7 +141,7 @@ class XEP_0115(BasePlugin): | ||||
|  | ||||
|         self.xmpp.event('entity_caps', p) | ||||
|  | ||||
|     async def _process_caps(self, pres): | ||||
|     async def _process_caps(self, pres: Presence): | ||||
|         if not pres['caps']['hash']: | ||||
|             log.debug("Received unsupported legacy caps: %s, %s, %s", | ||||
|                     pres['caps']['node'], | ||||
| @@ -147,7 +151,11 @@ class XEP_0115(BasePlugin): | ||||
|             return | ||||
|  | ||||
|         ver = pres['caps']['ver'] | ||||
|         async with self._locks[ver]: | ||||
|             await self._process_caps_wrapped(pres, ver) | ||||
|         self._locks.pop(ver, None) | ||||
|  | ||||
|     async def _process_caps_wrapped(self, pres: Presence, ver: str): | ||||
|         existing_verstring = await self.get_verstring(pres['from'].full) | ||||
|         if str(existing_verstring) == str(ver): | ||||
|             return | ||||
| @@ -162,7 +170,7 @@ class XEP_0115(BasePlugin): | ||||
|         if pres['caps']['hash'] not in self.hashes: | ||||
|             try: | ||||
|                 log.debug("Unknown caps hash: %s", pres['caps']['hash']) | ||||
|                 self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom) | ||||
|                 await self.xmpp['xep_0030'].get_info(jid=pres['from'], ifrom=ifrom) | ||||
|                 return | ||||
|             except XMPPError: | ||||
|                 return | ||||
|   | ||||
| @@ -60,7 +60,7 @@ class StaticCaps(object): | ||||
|             return False | ||||
|  | ||||
|         if node in (None, ''): | ||||
|             info = self.caps.get_caps(jid) | ||||
|             info = await self.caps.get_caps(jid) | ||||
|             if info and feature in info['features']: | ||||
|                 return True | ||||
|  | ||||
| @@ -134,7 +134,7 @@ class StaticCaps(object): | ||||
|     def get_verstring(self, jid, node, ifrom, data): | ||||
|         return self.jid_vers.get(jid, None) | ||||
|  | ||||
|     def get_caps(self, jid, node, ifrom, data): | ||||
|     async def get_caps(self, jid, node, ifrom, data): | ||||
|         verstring = data.get('verstring', None) | ||||
|         if verstring is None: | ||||
|             return None | ||||
|   | ||||
| @@ -8,7 +8,6 @@ import datetime as dt | ||||
|  | ||||
| from slixmpp.xmlstream import ElementBase | ||||
| from slixmpp.plugins import xep_0082 | ||||
| from slixmpp.thirdparty import tzutc, tzoffset | ||||
|  | ||||
|  | ||||
| class EntityTime(ElementBase): | ||||
| @@ -87,7 +86,7 @@ class EntityTime(ElementBase): | ||||
|                       seconds (positive or negative) to offset. | ||||
|         """ | ||||
|         time = xep_0082.time(offset=value) | ||||
|         if xep_0082.parse(time).tzinfo == tzutc(): | ||||
|         if xep_0082.parse(time).tzinfo == dt.timezone.utc: | ||||
|             self._set_sub_text('tzo', 'Z') | ||||
|         else: | ||||
|             self._set_sub_text('tzo', time[-6:]) | ||||
| @@ -111,6 +110,6 @@ class EntityTime(ElementBase): | ||||
|         date = value | ||||
|         if not isinstance(value, dt.datetime): | ||||
|             date = xep_0082.parse(value) | ||||
|         date = date.astimezone(tzutc()) | ||||
|         date = date.astimezone(dt.timezone.utc) | ||||
|         value = xep_0082.format_datetime(date) | ||||
|         self._set_sub_text('utc', value) | ||||
|   | ||||
| @@ -30,6 +30,10 @@ class Delay(ElementBase): | ||||
|  | ||||
|     def set_stamp(self, value): | ||||
|         if isinstance(value, dt.datetime): | ||||
|             if value.tzinfo is None: | ||||
|                 raise ValueError(f'Datetime provided without timezone information: {value}') | ||||
|             if value.tzinfo != dt.timezone.utc: | ||||
|                 value = value.astimezone(dt.timezone.utc) | ||||
|             value = xep_0082.format_datetime(value) | ||||
|         self._set_attr('stamp', value) | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,32 @@ log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0221(BasePlugin): | ||||
|     """ | ||||
|     XEP-0221: Data Forms Media Element | ||||
|  | ||||
|     In certain implementations of Data Forms (XEP-0004), it can be | ||||
|     helpful to include media data such as small images. One example is | ||||
|     CAPTCHA Forms (XEP-0158). This plugin implements a method for | ||||
|     including media data in a data form. | ||||
|  | ||||
|     Typical use pattern: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         self.register_plugin('xep_0221') | ||||
|         self['xep_0050'].add_command(node="showimage", | ||||
|                                         name="Show my image", | ||||
|                                         handler=self.form_handler) | ||||
|  | ||||
|         def form_handler(self,iq,session): | ||||
|             image_url="https://xmpp.org/images/logos/xmpp-logo.svg" | ||||
|             form=self['xep_0004'].make_form('result','My Image') | ||||
|             form.addField(var='myimage', ftype='text-single', label='My Image', value=image_url) | ||||
|             form.field['myimage']['media'].add_uri(value=image_url, itype="image/svg") | ||||
|             session['payload']=form | ||||
|             return session | ||||
|     """ | ||||
|  | ||||
|  | ||||
|     name = 'xep_0221' | ||||
|     description = 'XEP-0221: Data Forms Media Element' | ||||
|   | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0234/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0234/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .jingle_file_transfer import XEP_0234 | ||||
|  | ||||
| register_plugin(XEP_0234) | ||||
							
								
								
									
										21
									
								
								slixmpp/plugins/xep_0234/jingle_file_transfer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								slixmpp/plugins/xep_0234/jingle_file_transfer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import logging | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0234(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0234: Jingle File Transfer | ||||
|  | ||||
|     Minimum needed for xep 0385 (Stateless inline media sharing) | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0234" | ||||
|     description = "XEP-0234: Jingle File Transfer" | ||||
|     dependencies = {"xep_0082", "xep_0300"} | ||||
|     stanza = stanza | ||||
							
								
								
									
										38
									
								
								slixmpp/plugins/xep_0234/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								slixmpp/plugins/xep_0234/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from slixmpp.plugins.xep_0082 import format_datetime, parse | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NS = "urn:xmpp:jingle:apps:file-transfer:5" | ||||
|  | ||||
|  | ||||
| class File(ElementBase): | ||||
|     name = "file" | ||||
|     namespace = NS | ||||
|     plugin_attrib = "file" | ||||
|     interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"} | ||||
|  | ||||
|     def set_size(self, size: int): | ||||
|         self._set_sub_text("size", str(size)) | ||||
|  | ||||
|     def get_size(self): | ||||
|         return _int_or_none(self._get_sub_text("size")) | ||||
|  | ||||
|     def get_date(self): | ||||
|         try: | ||||
|             return parse(self._get_sub_text("date")) | ||||
|         except ValueError: | ||||
|             return | ||||
|  | ||||
|     def set_date(self, stamp: datetime): | ||||
|         try: | ||||
|             self._set_sub_text("date", format_datetime(stamp)) | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| def _int_or_none(v): | ||||
|     try: | ||||
|         return int(v) | ||||
|     except ValueError: | ||||
|         return None | ||||
							
								
								
									
										5
									
								
								slixmpp/plugins/xep_0264/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								slixmpp/plugins/xep_0264/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from .thumbnail import XEP_0264 | ||||
|  | ||||
| register_plugin(XEP_0264) | ||||
							
								
								
									
										36
									
								
								slixmpp/plugins/xep_0264/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								slixmpp/plugins/xep_0264/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import register_stanza_plugin | ||||
| from slixmpp.plugins.xep_0234.stanza import File | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NS = "urn:xmpp:thumbs:1" | ||||
|  | ||||
|  | ||||
| class Thumbnail(ElementBase): | ||||
|     name = plugin_attrib = "thumbnail" | ||||
|     namespace = NS | ||||
|     interfaces = {"uri", "media-type", "width", "height"} | ||||
|  | ||||
|     def get_width(self) -> int: | ||||
|         return _int_or_none(self._get_attr("width")) | ||||
|  | ||||
|     def get_height(self) -> int: | ||||
|         return _int_or_none(self._get_attr("height")) | ||||
|  | ||||
|     def set_width(self, v: int) -> None: | ||||
|         self._set_attr("width", str(v)) | ||||
|  | ||||
|     def set_height(self, v: int) -> None: | ||||
|         self._set_attr("height", str(v)) | ||||
|  | ||||
|  | ||||
| def _int_or_none(v) -> Optional[int]: | ||||
|     try: | ||||
|         return int(v) | ||||
|     except ValueError: | ||||
|         return None | ||||
|  | ||||
|  | ||||
| def register_plugin(): | ||||
|     register_stanza_plugin(File, Thumbnail) | ||||
							
								
								
									
										24
									
								
								slixmpp/plugins/xep_0264/thumbnail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								slixmpp/plugins/xep_0264/thumbnail.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import logging | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0264(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0264: Jingle Content Thumbnails | ||||
|  | ||||
|     Can also be used with 0385 (Stateless inline media sharing) | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0264" | ||||
|     description = "XEP-0264: Jingle Content Thumbnails" | ||||
|     dependencies = {"xep_0234"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         stanza.register_plugin() | ||||
							
								
								
									
										5
									
								
								slixmpp/plugins/xep_0292/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								slixmpp/plugins/xep_0292/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza, vcard4 | ||||
|  | ||||
| register_plugin(vcard4.XEP_0292) | ||||
							
								
								
									
										167
									
								
								slixmpp/plugins/xep_0292/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								slixmpp/plugins/xep_0292/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| import datetime | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import ElementBase, Iq, register_stanza_plugin | ||||
|  | ||||
| NS = "urn:ietf:params:xml:ns:vcard-4.0" | ||||
|  | ||||
|  | ||||
| class _VCardElementBase(ElementBase): | ||||
|     namespace = NS | ||||
|  | ||||
|  | ||||
| class VCard4(_VCardElementBase): | ||||
|     name = plugin_attrib = "vcard" | ||||
|     interfaces = {"full_name", "given", "surname", "birthday"} | ||||
|  | ||||
|     def set_full_name(self, full_name: str): | ||||
|         self["fn"]["text"] = full_name | ||||
|  | ||||
|     def get_full_name(self): | ||||
|         return self["fn"]["text"] | ||||
|  | ||||
|     def set_given(self, given: str): | ||||
|         self["n"]["given"] = given | ||||
|  | ||||
|     def get_given(self): | ||||
|         return self["n"]["given"] | ||||
|  | ||||
|     def set_surname(self, surname: str): | ||||
|         self["n"]["surname"] = surname | ||||
|  | ||||
|     def get_surname(self): | ||||
|         return self["n"]["surname"] | ||||
|  | ||||
|     def set_birthday(self, birthday: datetime.date): | ||||
|         self["bday"]["date"] = birthday | ||||
|  | ||||
|     def get_birthday(self): | ||||
|         return self["bday"]["date"] | ||||
|  | ||||
|     def add_tel(self, number: str, name: Optional[str] = None): | ||||
|         tel = Tel() | ||||
|         if name: | ||||
|             tel["parameters"]["type_"]["text"] = name | ||||
|         tel["uri"] = f"tel:{number}" | ||||
|         self.append(tel) | ||||
|  | ||||
|     def add_address( | ||||
|         self, country: Optional[str] = None, locality: Optional[str] = None | ||||
|     ): | ||||
|         adr = Adr() | ||||
|         if locality: | ||||
|             adr["locality"] = locality | ||||
|         if country: | ||||
|             adr["country"] = country | ||||
|         self.append(adr) | ||||
|  | ||||
|     def add_nickname(self, nick: str): | ||||
|         el = Nickname() | ||||
|         el["text"] = nick | ||||
|         self.append(el) | ||||
|  | ||||
|     def add_note(self, note: str): | ||||
|         el = Note() | ||||
|         el["text"] = note | ||||
|         self.append(el) | ||||
|  | ||||
|     def add_impp(self, impp: str): | ||||
|         el = Impp() | ||||
|         el["uri"] = impp | ||||
|         self.append(el) | ||||
|  | ||||
|     def add_url(self, url: str): | ||||
|         el = Url() | ||||
|         el["uri"] = url | ||||
|         self.append(el) | ||||
|  | ||||
|     def add_email(self, email: str): | ||||
|         el = Email() | ||||
|         el["text"] = email | ||||
|         self.append(el) | ||||
|  | ||||
|  | ||||
| class _VCardTextElementBase(_VCardElementBase): | ||||
|     interfaces = {"text"} | ||||
|     sub_interfaces = {"text"} | ||||
|  | ||||
|  | ||||
| class Fn(_VCardTextElementBase): | ||||
|     name = plugin_attrib = "fn" | ||||
|  | ||||
|  | ||||
| class Nickname(_VCardTextElementBase): | ||||
|     name = plugin_attrib = "nickname" | ||||
|  | ||||
|  | ||||
| class Note(_VCardTextElementBase): | ||||
|     name = plugin_attrib = "note" | ||||
|  | ||||
|  | ||||
| class _VCardUriElementBase(_VCardElementBase): | ||||
|     interfaces = {"uri"} | ||||
|     sub_interfaces = {"uri"} | ||||
|  | ||||
|  | ||||
| class Url(_VCardUriElementBase): | ||||
|     name = plugin_attrib = "url" | ||||
|  | ||||
|  | ||||
| class Impp(_VCardUriElementBase): | ||||
|     name = plugin_attrib = "impp" | ||||
|  | ||||
|  | ||||
| class Email(_VCardTextElementBase): | ||||
|     name = plugin_attrib = "email" | ||||
|  | ||||
|  | ||||
| class N(_VCardElementBase): | ||||
|     name = "n" | ||||
|     plugin_attrib = "n" | ||||
|     interfaces = sub_interfaces = {"given", "surname", "additional"} | ||||
|  | ||||
|  | ||||
| class BDay(_VCardElementBase): | ||||
|     name = plugin_attrib = "bday" | ||||
|     interfaces = {"date"} | ||||
|  | ||||
|     def set_date(self, date: datetime.date): | ||||
|         d = Date() | ||||
|         d.xml.text = date.strftime("%Y-%m-%d") | ||||
|         self.append(d) | ||||
|  | ||||
|     def get_date(self): | ||||
|         for elem in self.xml: | ||||
|             try: | ||||
|                 return datetime.date.fromisoformat(elem.text) | ||||
|             except ValueError: | ||||
|                 return None | ||||
|  | ||||
|  | ||||
| class Date(_VCardElementBase): | ||||
|     name = "date" | ||||
|  | ||||
|  | ||||
| class Tel(_VCardUriElementBase): | ||||
|     name = plugin_attrib = "tel" | ||||
|  | ||||
|  | ||||
| class Parameters(_VCardElementBase): | ||||
|     name = plugin_attrib = "parameters" | ||||
|  | ||||
|  | ||||
| class Type(_VCardTextElementBase): | ||||
|     name = "type" | ||||
|     plugin_attrib = "type_" | ||||
|  | ||||
|  | ||||
| class Adr(_VCardElementBase): | ||||
|     name = plugin_attrib = "adr" | ||||
|     interfaces = sub_interfaces = {"locality", "country"} | ||||
|  | ||||
|  | ||||
| register_stanza_plugin(Parameters, Type) | ||||
| register_stanza_plugin(Tel, Parameters) | ||||
| for p in N, Fn, Nickname, Note, Url, Impp, Email, BDay, Tel, Adr: | ||||
|     register_stanza_plugin(VCard4, p, iterable=True) | ||||
| register_stanza_plugin(Iq, VCard4) | ||||
							
								
								
									
										111
									
								
								slixmpp/plugins/xep_0292/vcard4.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								slixmpp/plugins/xep_0292/vcard4.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| import logging | ||||
| from datetime import date | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import ( | ||||
|     JID, | ||||
|     ComponentXMPP, | ||||
|     register_stanza_plugin, | ||||
| ) | ||||
| from slixmpp.plugins.base import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0292(BasePlugin): | ||||
|     """ | ||||
|     vCard4 over XMPP | ||||
|  | ||||
|     Does not implement the IQ semantics that neither movim does gajim implement, | ||||
|     cf https://xmpp.org/extensions/xep-0292.html#self-iq-retrieval and | ||||
|     https://xmpp.org/extensions/xep-0292.html#self-iq-publication | ||||
|  | ||||
|     Does not implement the "empty pubsub event item" as a notification mechanism, | ||||
|     that neither gajim nor movim implement | ||||
|     https://xmpp.org/extensions/xep-0292.html#sect-idm45744791178720 | ||||
|  | ||||
|     Relies on classic pubsub semantics instead. | ||||
|     """ | ||||
|     xmpp: ComponentXMPP | ||||
|  | ||||
|     name = "xep_0292" | ||||
|     description = "vCard4 Over XMPP" | ||||
|     dependencies = {"xep_0163", "xep_0060", "xep_0030"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         pubsub_stanza = self.xmpp["xep_0060"].stanza | ||||
|  | ||||
|         register_stanza_plugin(pubsub_stanza.Item, stanza.VCard4) | ||||
|         register_stanza_plugin(pubsub_stanza.EventItem, stanza.VCard4) | ||||
|  | ||||
|         self.xmpp['xep_0060'].map_node_event(stanza.NS, 'vcard4') | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp['xep_0030'].del_feature(feature=stanza.NS) | ||||
|         self.xmpp['xep_0163'].remove_interest(stanza.NS) | ||||
|  | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp['xep_0163'].register_pep('vcard4', stanza.VCard4) | ||||
|  | ||||
|     def publish_vcard( | ||||
|         self, | ||||
|         full_name: Optional[str] = None, | ||||
|         given: Optional[str] = None, | ||||
|         surname: Optional[str] = None, | ||||
|         birthday: Optional[date] = None, | ||||
|         nickname: Optional[str] = None, | ||||
|         phone: Optional[str] = None, | ||||
|         note: Optional[str] = None, | ||||
|         url: Optional[str] = None, | ||||
|         email: Optional[str] = None, | ||||
|         country: Optional[str] = None, | ||||
|         locality: Optional[str] = None, | ||||
|         impp: Optional[str] = None, | ||||
|         **pubsubkwargs, | ||||
|     ): | ||||
|         """ | ||||
|         Publish a vcard using PEP | ||||
|         """ | ||||
|         vcard = stanza.VCard4() | ||||
|  | ||||
|         if impp: | ||||
|             vcard.add_impp(impp) | ||||
|  | ||||
|         if nickname: | ||||
|             vcard.add_nickname(nickname) | ||||
|         if full_name: | ||||
|             vcard["full_name"] = full_name | ||||
|  | ||||
|         if given: | ||||
|             vcard["given"] = given | ||||
|         if surname: | ||||
|             vcard["surname"] = surname | ||||
|         if birthday: | ||||
|             vcard["birthday"] = birthday | ||||
|  | ||||
|         if note: | ||||
|             vcard.add_note(note) | ||||
|         if url: | ||||
|             vcard.add_url(url) | ||||
|         if email: | ||||
|             vcard.add_email(email) | ||||
|         if phone: | ||||
|             vcard.add_tel(phone) | ||||
|         if country and locality: | ||||
|             vcard.add_address(country, locality) | ||||
|         elif country: | ||||
|             vcard.add_address(country, locality) | ||||
|  | ||||
|         return self.xmpp["xep_0163"].publish(vcard, id="current", **pubsubkwargs) | ||||
|  | ||||
|     def retrieve_vcard(self, jid: JID, **pubsubkwargs): | ||||
|         """ | ||||
|         Retrieve a vcard using PEP | ||||
|         """ | ||||
|         return self.xmpp["xep_0060"].get_item( | ||||
|             jid, stanza.VCard4.namespace, "current", **pubsubkwargs | ||||
|         ) | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
| @@ -52,9 +52,10 @@ class MAM(ElementBase): | ||||
|     #:   fetch, not relevant for the stanza itself. | ||||
|     interfaces = { | ||||
|         'queryid', 'start', 'end', 'with', 'results', | ||||
|         'before_id', 'after_id', 'ids', | ||||
|         'before_id', 'after_id', 'ids', 'flip_page', | ||||
|     } | ||||
|     sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids'} | ||||
|     sub_interfaces = {'start', 'end', 'with', 'before_id', 'after_id', 'ids', | ||||
|                       'flip_page'} | ||||
|  | ||||
|     def setup(self, xml=None): | ||||
|         ElementBase.setup(self, xml) | ||||
| @@ -81,7 +82,7 @@ class MAM(ElementBase): | ||||
|     def get_start(self) -> Optional[datetime]: | ||||
|         fields = self.get_fields() | ||||
|         field = fields.get('start') | ||||
|         if field: | ||||
|         if field and field["value"]: | ||||
|             return xep_0082.parse(field['value']) | ||||
|         return None | ||||
|  | ||||
| @@ -94,7 +95,7 @@ class MAM(ElementBase): | ||||
|     def get_end(self) -> Optional[datetime]: | ||||
|         fields = self.get_fields() | ||||
|         field = fields.get('end') | ||||
|         if field: | ||||
|         if field and field["value"]: | ||||
|             return xep_0082.parse(field['value']) | ||||
|         return None | ||||
|  | ||||
| @@ -168,6 +169,8 @@ class MAM(ElementBase): | ||||
|     def del_results(self): | ||||
|         self._results = [] | ||||
|  | ||||
|     def get_flip_page(self): | ||||
|         return self.xml.find(f'{{{self.namespace}}}flip-page') is not None | ||||
|  | ||||
| class Fin(ElementBase): | ||||
|     """A MAM fin element (end of query). | ||||
| @@ -187,7 +190,7 @@ class Fin(ElementBase): | ||||
|     name = 'fin' | ||||
|     namespace = 'urn:xmpp:mam:2' | ||||
|     plugin_attrib = 'mam_fin' | ||||
|     interfaces = {'results'} | ||||
|     interfaces = {'results', 'stable', 'complete'} | ||||
|  | ||||
|     def setup(self, xml=None): | ||||
|         ElementBase.setup(self, xml) | ||||
|   | ||||
							
								
								
									
										11
									
								
								slixmpp/plugins/xep_0317/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								slixmpp/plugins/xep_0317/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| # Slixmpp: The Slick XMPP Library | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| from slixmpp.plugins import register_plugin | ||||
| from slixmpp.plugins.xep_0317 import stanza | ||||
| from slixmpp.plugins.xep_0317.hats import XEP_0317 | ||||
| from slixmpp.plugins.xep_0317.stanza import Hat, Hats | ||||
|  | ||||
| register_plugin(XEP_0317) | ||||
|  | ||||
| __all__ = ['stanza', 'XEP_317'] | ||||
							
								
								
									
										16
									
								
								slixmpp/plugins/xep_0317/hats.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								slixmpp/plugins/xep_0317/hats.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0317(BasePlugin): | ||||
|     """ | ||||
|     XEP-0317: Hats | ||||
|     """ | ||||
|     name = 'xep_0317' | ||||
|     description = 'XEP-0317: Hats' | ||||
|     dependencies = {'xep_0030', 'xep_0045', 'xep_0050'} | ||||
|     stanza = stanza | ||||
|     namespace = stanza.NS | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         stanza.register_plugin() | ||||
							
								
								
									
										58
									
								
								slixmpp/plugins/xep_0317/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								slixmpp/plugins/xep_0317/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| from slixmpp import Presence | ||||
| from slixmpp.xmlstream import ElementBase, register_stanza_plugin | ||||
| from typing import List, Tuple | ||||
|  | ||||
| NS = 'urn:xmpp:hats:0' | ||||
|  | ||||
|  | ||||
| class Hats(ElementBase): | ||||
|     """ | ||||
|     Hats element, container for multiple hats: | ||||
|  | ||||
|     .. code-block::xml | ||||
|  | ||||
|  | ||||
|       <hats xmlns='urn:xmpp:hats:0'> | ||||
|         <hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'> | ||||
|             <badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/> | ||||
|         </hat> | ||||
|         <hat title='Presenter' uri='http://schemas.example.com/hats#presenter' xml:lang='en-us'> | ||||
|             <badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#EC0524"/> | ||||
|         </hat> | ||||
|       </hats> | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     name = 'hats' | ||||
|     namespace = NS | ||||
|     plugin_attrib = 'hats' | ||||
|  | ||||
|     def add_hats(self, data: List[Tuple[str, str]]) -> None: | ||||
|         for uri, title in data: | ||||
|             hat = Hat() | ||||
|             hat["uri"] = uri | ||||
|             hat["title"] = title | ||||
|             self.append(hat) | ||||
|  | ||||
|  | ||||
| class Hat(ElementBase): | ||||
|     """ | ||||
|     Hat element, has a title and url, may contain arbitrary sub-elements. | ||||
|  | ||||
|     .. code-block::xml | ||||
|  | ||||
|         <hat title='Host' uri='http://schemas.example.com/hats#host' xml:lang='en-us'> | ||||
|             <badge xmlns="urn:example:badges" fgcolor="#000000" bgcolor="#58C5BA"/> | ||||
|         </hat> | ||||
|  | ||||
|     """ | ||||
|     name = 'hat' | ||||
|     plugin_attrib = 'hat' | ||||
|     namespace = NS | ||||
|     interfaces = {'title', 'uri'} | ||||
|     plugin_multi_attrib = "hats" | ||||
|  | ||||
|  | ||||
| def register_plugin() -> None: | ||||
|     register_stanza_plugin(Hats, Hat, iterable=True) | ||||
|     register_stanza_plugin(Presence, Hats) | ||||
| @@ -1,4 +1,3 @@ | ||||
|  | ||||
| # slixmpp: The Slick XMPP Library | ||||
| # Copyright (C) 2016 Emmanuel Gil Peyrot | ||||
| # This file is part of slixmpp. | ||||
| @@ -68,11 +67,11 @@ class XEP_0333(BasePlugin): | ||||
|         :param JID mto: recipient of the marker | ||||
|         :param str id: Identifier of the marked message | ||||
|         :param str marker: Marker to send (one of | ||||
|             displayed, retrieved, or acknowledged) | ||||
|             displayed, received, or acknowledged) | ||||
|         :param str thread: Message thread | ||||
|         :param str mfrom: Use a specific JID to send the message | ||||
|         """ | ||||
|         if marker not in ('displayed', 'retrieved', 'acknowledged'): | ||||
|         if marker not in ('displayed', 'received', 'acknowledged'): | ||||
|             raise ValueError('Invalid marker: %s' % marker) | ||||
|         msg = self.xmpp.make_message(mto=mto, mfrom=mfrom) | ||||
|         if thread: | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0356 import stanza | ||||
| from slixmpp.plugins.xep_0356.stanza import Perm, Privilege | ||||
| from slixmpp.plugins.xep_0356.privilege import XEP_0356 | ||||
| from . import stanza | ||||
| from .privilege import XEP_0356 | ||||
| from .stanza import Perm, Privilege | ||||
|  | ||||
| register_plugin(XEP_0356) | ||||
|   | ||||
							
								
								
									
										36
									
								
								slixmpp/plugins/xep_0356/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								slixmpp/plugins/xep_0356/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import dataclasses | ||||
| from collections import defaultdict | ||||
| from enum import Enum | ||||
|  | ||||
|  | ||||
| class RosterAccess(str, Enum): | ||||
|     NONE = "none" | ||||
|     GET = "get" | ||||
|     SET = "set" | ||||
|     BOTH = "both" | ||||
|  | ||||
|  | ||||
| class MessagePermission(str, Enum): | ||||
|     NONE = "none" | ||||
|     OUTGOING = "outgoing" | ||||
|  | ||||
|  | ||||
| class IqPermission(str, Enum): | ||||
|     NONE = "none" | ||||
|     GET = "get" | ||||
|     SET = "set" | ||||
|     BOTH = "both" | ||||
|  | ||||
|  | ||||
| class PresencePermission(str, Enum): | ||||
|     NONE = "none" | ||||
|     MANAGED_ENTITY = "managed_entity" | ||||
|     ROSTER = "roster" | ||||
|  | ||||
|  | ||||
| @dataclasses.dataclass | ||||
| class Permissions: | ||||
|     roster = RosterAccess.NONE | ||||
|     message = MessagePermission.NONE | ||||
|     iq = defaultdict(lambda: IqPermission.NONE) | ||||
|     presence = PresencePermission.NONE | ||||
| @@ -1,14 +1,16 @@ | ||||
| import logging | ||||
| import typing | ||||
| import uuid | ||||
| from collections import defaultdict | ||||
|  | ||||
| from slixmpp import Message, JID, Iq | ||||
| from slixmpp import JID, Iq, Message | ||||
| from slixmpp.plugins.base import BasePlugin | ||||
| from slixmpp.xmlstream.matcher import StanzaPath | ||||
| from slixmpp.xmlstream import StanzaBase | ||||
| from slixmpp.xmlstream.handler import Callback | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0356 import stanza, Privilege, Perm | ||||
| from slixmpp.xmlstream.matcher import StanzaPath | ||||
|  | ||||
| from . import stanza | ||||
| from .permissions import IqPermission, MessagePermission, Permissions, RosterAccess | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -29,7 +31,7 @@ class XEP_0356(BasePlugin): | ||||
|     dependencies = {"xep_0297"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     granted_privileges = {"roster": "none", "message": "none", "presence": "none"} | ||||
|     granted_privileges = defaultdict(Permissions) | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         if not self.xmpp.is_component: | ||||
| @@ -49,32 +51,42 @@ class XEP_0356(BasePlugin): | ||||
|     def plugin_end(self): | ||||
|         self.xmpp.remove_handler("Privileges") | ||||
|  | ||||
|     def _handle_privilege(self, msg: Message): | ||||
|     def _handle_privilege(self, msg: StanzaBase): | ||||
|         """ | ||||
|         Called when the XMPP server advertise the component's privileges. | ||||
|  | ||||
|         Stores the privileges in this instance's granted_privileges attribute (a dict) | ||||
|         and raises the privileges_advertised event | ||||
|         """ | ||||
|         permissions = self.granted_privileges[msg.get_from()] | ||||
|         for perm in msg["privilege"]["perms"]: | ||||
|             self.granted_privileges[perm["access"]] = perm["type"] | ||||
|             access = perm["access"] | ||||
|             if access == "iq": | ||||
|                 for ns in perm["namespaces"]: | ||||
|                     permissions.iq[ns["ns"]] = ns["type"] | ||||
|             elif access in _VALID_ACCESSES: | ||||
|                 setattr(permissions, access, perm["type"]) | ||||
|             else: | ||||
|                 log.warning("Received an invalid privileged access: %s", access) | ||||
|         log.debug(f"Privileges: {self.granted_privileges}") | ||||
|         self.xmpp.event("privileges_advertised") | ||||
|  | ||||
|     def send_privileged_message(self, msg: Message): | ||||
|         if self.granted_privileges["message"] == "outgoing": | ||||
|             self._make_privileged_message(msg).send() | ||||
|         else: | ||||
|             log.error( | ||||
|         if ( | ||||
|             self.granted_privileges[msg.get_from().domain].message | ||||
|             != MessagePermission.OUTGOING | ||||
|         ): | ||||
|             raise PermissionError( | ||||
|                 "The server hasn't authorized us to send messages on behalf of other users" | ||||
|             ) | ||||
|         else: | ||||
|             self._make_privileged_message(msg).send() | ||||
|  | ||||
|     def _make_privileged_message(self, msg: Message): | ||||
|         stanza = self.xmpp.make_message( | ||||
|             mto=self.xmpp.server_host, mfrom=self.xmpp.boundjid.bare | ||||
|         ) | ||||
|         stanza["privilege"]["forwarded"].append(msg) | ||||
|         return stanza | ||||
|         server = msg.get_from().domain | ||||
|         wrapped = self.xmpp.make_message(mto=server, mfrom=self.xmpp.boundjid.bare) | ||||
|         wrapped["privilege"]["forwarded"].append(msg) | ||||
|         return wrapped | ||||
|  | ||||
|     def _make_get_roster(self, jid: typing.Union[JID, str], **iq_kwargs): | ||||
|         return self.xmpp.make_iq_get( | ||||
| @@ -106,9 +118,15 @@ class XEP_0356(BasePlugin): | ||||
|  | ||||
|         :param jid: user we want to fetch the roster from | ||||
|         """ | ||||
|         if self.granted_privileges["roster"] not in ("get", "both"): | ||||
|             log.error("The server did not grant us privileges to get rosters") | ||||
|             raise ValueError | ||||
|         if isinstance(jid, str): | ||||
|             jid = JID(jid) | ||||
|         if self.granted_privileges[jid.domain].roster not in ( | ||||
|             RosterAccess.GET, | ||||
|             RosterAccess.BOTH, | ||||
|         ): | ||||
|             raise PermissionError( | ||||
|                 "The server did not grant us privileges to get rosters" | ||||
|             ) | ||||
|         else: | ||||
|             return await self._make_get_roster(jid).send(**send_kwargs) | ||||
|  | ||||
| @@ -137,8 +155,56 @@ class XEP_0356(BasePlugin): | ||||
|             }, | ||||
|         } | ||||
|         """ | ||||
|         if self.granted_privileges["roster"] not in ("set", "both"): | ||||
|             log.error("The server did not grant us privileges to set rosters") | ||||
|             raise ValueError | ||||
|         if isinstance(jid, str): | ||||
|             jid = JID(jid) | ||||
|         if self.granted_privileges[jid.domain].roster not in ( | ||||
|             RosterAccess.GET, | ||||
|             RosterAccess.BOTH, | ||||
|         ): | ||||
|             raise PermissionError( | ||||
|                 "The server did not grant us privileges to set rosters" | ||||
|             ) | ||||
|         else: | ||||
|             return await self._make_set_roster(jid, roster_items).send(**send_kwargs) | ||||
|  | ||||
|     async def send_privileged_iq( | ||||
|         self, encapsulated_iq: Iq, iq_id: typing.Optional[str] = None | ||||
|     ): | ||||
|         """ | ||||
|         Send an IQ on behalf of a user | ||||
|  | ||||
|         Caution: the IQ *must* have the jabber:client namespace | ||||
|         """ | ||||
|         iq_id = iq_id or str(uuid.uuid4()) | ||||
|         encapsulated_iq["id"] = iq_id | ||||
|         server = encapsulated_iq.get_to().domain | ||||
|         perms = self.granted_privileges.get(server) | ||||
|         if not perms: | ||||
|             raise PermissionError(f"{server} has not granted us any privilege") | ||||
|         itype = encapsulated_iq["type"] | ||||
|         for ns in encapsulated_iq.plugins.values(): | ||||
|             type_ = perms.iq[ns.namespace] | ||||
|             if type_ == IqPermission.NONE: | ||||
|                 raise PermissionError( | ||||
|                     f"{server} has not granted any IQ privilege for namespace {ns.namespace}" | ||||
|                 ) | ||||
|             elif type_ == IqPermission.BOTH: | ||||
|                 pass | ||||
|             elif type_ != itype: | ||||
|                 raise PermissionError( | ||||
|                     f"{server} has not granted IQ {itype} privilege for namespace {ns.namespace}" | ||||
|                 ) | ||||
|         iq = self.xmpp.make_iq( | ||||
|             itype=itype, | ||||
|             ifrom=self.xmpp.boundjid.bare, | ||||
|             ito=encapsulated_iq.get_from(), | ||||
|             id=iq_id, | ||||
|         ) | ||||
|         iq["privileged_iq"].append(encapsulated_iq) | ||||
|  | ||||
|         resp = await iq.send() | ||||
|         return resp["privilege"]["forwarded"]["iq"] | ||||
|  | ||||
|  | ||||
| # does not include iq access that is handled differently | ||||
| _VALID_ACCESSES = {"message", "roster", "presence"} | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream import ( | ||||
|     ElementBase, | ||||
|     register_stanza_plugin, | ||||
| ) | ||||
| from slixmpp.plugins.xep_0297 import Forwarded | ||||
| from slixmpp.stanza import Iq, Message | ||||
| from slixmpp.xmlstream import ElementBase, register_stanza_plugin | ||||
|  | ||||
| NS = "urn:xmpp:privilege:2" | ||||
|  | ||||
|  | ||||
| class Privilege(ElementBase): | ||||
|     namespace = "urn:xmpp:privilege:1" | ||||
|     namespace = NS | ||||
|     name = "privilege" | ||||
|     plugin_attrib = "privilege" | ||||
|  | ||||
| @@ -24,24 +23,41 @@ class Privilege(ElementBase): | ||||
|  | ||||
|     def presence(self): | ||||
|         return self.permission("presence") | ||||
|      | ||||
|     def add_perm(self, access, type): | ||||
|  | ||||
|     def add_perm(self, access, type_): | ||||
|         # This should only be needed for servers, so maybe out of scope for slixmpp | ||||
|         perm = Perm() | ||||
|         perm["type"] = type | ||||
|         perm["type"] = type_ | ||||
|         perm["access"] = access | ||||
|         self.append(perm) | ||||
|  | ||||
|  | ||||
| class Perm(ElementBase): | ||||
|     namespace = "urn:xmpp:privilege:1" | ||||
|     namespace = NS | ||||
|     name = "perm" | ||||
|     plugin_attrib = "perm" | ||||
|     plugin_multi_attrib = "perms" | ||||
|     interfaces = {"type", "access"} | ||||
|  | ||||
|  | ||||
| class NameSpace(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "namespace" | ||||
|     plugin_attrib = "namespace" | ||||
|     plugin_multi_attrib = "namespaces" | ||||
|     interfaces = {"ns", "type"} | ||||
|  | ||||
|  | ||||
| class PrivilegedIq(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "privileged_iq" | ||||
|     plugin_attrib = "privileged_iq" | ||||
|  | ||||
|  | ||||
| def register(): | ||||
|     register_stanza_plugin(Message, Privilege) | ||||
|     register_stanza_plugin(Iq, Privilege) | ||||
|     register_stanza_plugin(Privilege, Forwarded) | ||||
|     register_stanza_plugin(Privilege, Perm, iterable=True) | ||||
|     register_stanza_plugin(Privilege, Perm, iterable=True) | ||||
|     register_stanza_plugin(Perm, NameSpace, iterable=True) | ||||
|     register_stanza_plugin(Iq, PrivilegedIq) | ||||
|   | ||||
| @@ -14,6 +14,8 @@ from typing import ( | ||||
|     IO, | ||||
| ) | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| from slixmpp import JID, __version__ | ||||
| from slixmpp.stanza import Iq | ||||
| from slixmpp.plugins import BasePlugin | ||||
| @@ -99,12 +101,17 @@ class XEP_0363(BasePlugin): | ||||
|  | ||||
|         :param domain: Domain to disco to find a service. | ||||
|         """ | ||||
|         if domain is None and self.xmpp.is_component: | ||||
|             domain = self.xmpp.server_host | ||||
|  | ||||
|         results = await self.xmpp['xep_0030'].get_info_from_domain( | ||||
|             domain=domain, **iqkwargs | ||||
|         ) | ||||
|  | ||||
|         candidates = [] | ||||
|         for info in results: | ||||
|             if not info['disco_info']: | ||||
|                 continue | ||||
|             for identity in info['disco_info']['identities']: | ||||
|                 if identity[0] == 'store' and identity[1] == 'file': | ||||
|                     candidates.append(info) | ||||
| @@ -113,7 +120,7 @@ class XEP_0363(BasePlugin): | ||||
|                 if feature == Request.namespace: | ||||
|                     return info | ||||
|  | ||||
|     def request_slot(self, jid: JID, filename: str, size: int, | ||||
|     def request_slot(self, jid: JID, filename: Path, size: int, | ||||
|                     content_type: Optional[str] = None, *, | ||||
|                     ifrom: Optional[JID] = None, **iqkwargs) -> Future: | ||||
|         """Request an HTTP upload slot from a service. | ||||
| @@ -125,12 +132,12 @@ class XEP_0363(BasePlugin): | ||||
|         """ | ||||
|         iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom) | ||||
|         request = iq['http_upload_request'] | ||||
|         request['filename'] = filename | ||||
|         request['filename'] = str(filename) | ||||
|         request['size'] = str(size) | ||||
|         request['content-type'] = content_type or self.default_content_type | ||||
|         return iq.send(**iqkwargs) | ||||
|  | ||||
|     async def upload_file(self, filename: str, size: Optional[int] = None, | ||||
|     async def upload_file(self, filename: Path, size: Optional[int] = None, | ||||
|                           content_type: Optional[str] = None, *, | ||||
|                           input_file: Optional[IO[bytes]]=None, | ||||
|                           domain: Optional[JID] = None, | ||||
|   | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0372/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0372/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .references import XEP_0372 | ||||
|  | ||||
| register_plugin(XEP_0372) | ||||
							
								
								
									
										23
									
								
								slixmpp/plugins/xep_0372/references.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								slixmpp/plugins/xep_0372/references.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import logging | ||||
|  | ||||
| from slixmpp import Message, register_stanza_plugin | ||||
| from slixmpp.plugins import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0372(BasePlugin): | ||||
|     """ | ||||
|     XEP-0372: References | ||||
|  | ||||
|     Minimum needed for xep 0385 (Stateless inline media sharing) | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0372" | ||||
|     description = "XEP-0372: References" | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Message, stanza.Reference) | ||||
							
								
								
									
										9
									
								
								slixmpp/plugins/xep_0372/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								slixmpp/plugins/xep_0372/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NAMESPACE = "urn:xmpp:reference:0" | ||||
|  | ||||
|  | ||||
| class Reference(ElementBase): | ||||
|     name = plugin_attrib = "reference" | ||||
|     namespace = NAMESPACE | ||||
|     interfaces = {"type", "uri", "id", "begin", "end"} | ||||
| @@ -26,6 +26,9 @@ class XEP_0377(BasePlugin): | ||||
|     dependencies = {'xep_0030', 'xep_0191'} | ||||
|     stanza = stanza | ||||
|  | ||||
|     SPAM = 'urn:xmpp:reporting:spam' | ||||
|     ABUSE = 'urn:xmpp:reporting:abuse' | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Block, stanza.Report) | ||||
|         register_stanza_plugin(stanza.Report, stanza.Text) | ||||
|   | ||||
| @@ -13,58 +13,23 @@ class Report(ElementBase): | ||||
|     Example sub stanza: | ||||
|     :: | ||||
|  | ||||
|         <report xmlns="urn:xmpp:reporting:0"> | ||||
|         <report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:abuse"> | ||||
|           <text xml:lang="en"> | ||||
|             Never came trouble to my house like this. | ||||
|           </text> | ||||
|           <spam/> | ||||
|         </report> | ||||
|  | ||||
|     Stanza Interface: | ||||
|     :: | ||||
|     The reason attribute is mandatory. | ||||
|  | ||||
|         abuse    -- Flag the report as abuse | ||||
|         spam     -- Flag the report as spam | ||||
|         text     -- Add a reason to the report | ||||
|  | ||||
|     Only one <spam/> or <abuse/> element can be present at once. | ||||
|     """ | ||||
|     name = "report" | ||||
|     namespace = "urn:xmpp:reporting:0" | ||||
|     namespace = "urn:xmpp:reporting:1" | ||||
|     plugin_attrib = "report" | ||||
|     interfaces = ("spam", "abuse", "text") | ||||
|     interfaces = ("text", "reason") | ||||
|     sub_interfaces = {'text'} | ||||
|  | ||||
|     def _purge_spam(self): | ||||
|         spam = self.xml.findall('{%s}spam' % self.namespace) | ||||
|         for element in spam: | ||||
|             self.xml.remove(element) | ||||
|  | ||||
|     def _purge_abuse(self): | ||||
|         abuse = self.xml.findall('{%s}abuse' % self.namespace) | ||||
|         for element in abuse: | ||||
|             self.xml.remove(element) | ||||
|  | ||||
|     def get_spam(self): | ||||
|         return self.xml.find('{%s}spam' % self.namespace) is not None | ||||
|  | ||||
|     def set_spam(self, value): | ||||
|         self._purge_spam() | ||||
|         if bool(value): | ||||
|             self._purge_abuse() | ||||
|             self.xml.append(ET.Element('{%s}spam' % self.namespace)) | ||||
|  | ||||
|     def get_abuse(self): | ||||
|         return self.xml.find('{%s}abuse' % self.namespace) is not None | ||||
|  | ||||
|     def set_abuse(self, value): | ||||
|         self._purge_abuse() | ||||
|         if bool(value): | ||||
|             self._purge_spam() | ||||
|             self.xml.append(ET.Element('{%s}abuse' % self.namespace)) | ||||
|  | ||||
|  | ||||
| class Text(ElementBase): | ||||
|     name = "text" | ||||
|     plugin_attrib = "text" | ||||
|     namespace = "urn:xmpp:reporting:0" | ||||
|     namespace = "urn:xmpp:reporting:1" | ||||
|   | ||||
							
								
								
									
										11
									
								
								slixmpp/plugins/xep_0385/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								slixmpp/plugins/xep_0385/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
|  | ||||
| # Slixmpp: The Slick XMPP Library | ||||
| # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .sims import XEP_0385 | ||||
|  | ||||
| register_plugin(XEP_0385) | ||||
							
								
								
									
										66
									
								
								slixmpp/plugins/xep_0385/sims.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								slixmpp/plugins/xep_0385/sims.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import logging | ||||
| from datetime import datetime | ||||
| from pathlib import Path | ||||
| from typing import Iterable, Optional | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0385(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0385: Stateless Inline Media Sharing (SIMS) | ||||
|  | ||||
|     Only support outgoing SIMS, incoming is not handled at all. | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0385" | ||||
|     description = "XEP-0385: Stateless Inline Media Sharing (SIMS)" | ||||
|     dependencies = {"xep_0234", "xep_0300", "xep_0372"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(self.xmpp["xep_0372"].stanza.Reference, stanza.Sims) | ||||
|         register_stanza_plugin(Message, stanza.Sims) | ||||
|  | ||||
|         register_stanza_plugin(stanza.Sims, stanza.Sources) | ||||
|         register_stanza_plugin(stanza.Sims, self.xmpp["xep_0234"].stanza.File) | ||||
|         register_stanza_plugin(stanza.Sources, self.xmpp["xep_0372"].stanza.Reference) | ||||
|  | ||||
|     def get_sims( | ||||
|         self, | ||||
|         path: Path, | ||||
|         uris: Iterable[str], | ||||
|         media_type: Optional[str], | ||||
|         desc: Optional[str], | ||||
|     ): | ||||
|         sims = stanza.Sims() | ||||
|         for uri in uris: | ||||
|             ref = self.xmpp["xep_0372"].stanza.Reference() | ||||
|             ref["uri"] = uri | ||||
|             ref["type"] = "data" | ||||
|             sims["sources"].append(ref) | ||||
|         if media_type: | ||||
|             sims["file"]["media-type"] = media_type | ||||
|         if desc: | ||||
|             sims["file"]["desc"] = desc | ||||
|         sims["file"]["name"] = path.name | ||||
|  | ||||
|         stat = path.stat() | ||||
|         sims["file"]["size"] = stat.st_size | ||||
|         sims["file"]["date"] = datetime.fromtimestamp(stat.st_mtime) | ||||
|  | ||||
|         h = self.xmpp.plugin["xep_0300"].compute_hash(path) | ||||
|         h["value"] = h["value"].decode() | ||||
|         sims["file"].append(h) | ||||
|  | ||||
|         ref = self.xmpp["xep_0372"].stanza.Reference() | ||||
|         ref.append(sims) | ||||
|         ref["type"] = "data" | ||||
|         return ref | ||||
							
								
								
									
										14
									
								
								slixmpp/plugins/xep_0385/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								slixmpp/plugins/xep_0385/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NAMESPACE = "urn:xmpp:sims:1" | ||||
|  | ||||
|  | ||||
| class Sims(ElementBase): | ||||
|     name = "media-sharing" | ||||
|     plugin_attrib = "sims" | ||||
|     namespace = NAMESPACE | ||||
|  | ||||
|  | ||||
| class Sources(ElementBase): | ||||
|     name = plugin_attrib = "sources" | ||||
|     namespace = NAMESPACE | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0402/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0402/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .bookmarks import XEP_0402 | ||||
|  | ||||
| register_plugin(XEP_0402) | ||||
							
								
								
									
										18
									
								
								slixmpp/plugins/xep_0402/bookmarks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								slixmpp/plugins/xep_0402/bookmarks.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| from slixmpp.plugins import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0402(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0402: PEP Native bookmarks | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0402" | ||||
|     description = "XEP-0402: PEP Native bookmarks" | ||||
|     dependencies = {"xep_0402"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         stanza.register_plugin() | ||||
							
								
								
									
										33
									
								
								slixmpp/plugins/xep_0402/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								slixmpp/plugins/xep_0402/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| from slixmpp import register_stanza_plugin | ||||
| from slixmpp.plugins.xep_0060.stanza import Item | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NS = "urn:xmpp:bookmarks:1" | ||||
|  | ||||
|  | ||||
| class Conference(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "conference" | ||||
|     plugin_attrib = "conference" | ||||
|     interfaces = {"name", "autojoin", "nick", "password"} | ||||
|     sub_interfaces = {"nick", "password"} | ||||
|  | ||||
|     def set_autojoin(self, v: bool): | ||||
|         self._set_attr("autojoin", "true" if v else "false") | ||||
|  | ||||
|     def get_autojoin(self): | ||||
|         v = self._get_attr("autojoin", "") | ||||
|         if not v: | ||||
|             return False | ||||
|         return v == "1" or v.lower() == "true" | ||||
|  | ||||
|  | ||||
| class Extensions(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "extensions" | ||||
|     plugin_attrib = "extensions" | ||||
|  | ||||
|  | ||||
| def register_plugin(): | ||||
|     register_stanza_plugin(Conference, Extensions) | ||||
|     register_stanza_plugin(Item, Conference) | ||||
| @@ -1,8 +1,13 @@ | ||||
|  | ||||
| # Slixmpp: The Slick XMPP Library | ||||
| # Copyright (C) 2020 Mathieu Pasquet <mathieui@mathieui.net> | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permissio | ||||
| from abc import ABC | ||||
| try: | ||||
|     from typing import Literal | ||||
| except ImportError: | ||||
|     from typing_extensions import Literal | ||||
|  | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream import ( | ||||
|     ElementBase, | ||||
| @@ -10,14 +15,83 @@ from slixmpp.xmlstream import ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| NS = 'urn:xmpp:fallback:0' | ||||
| NS = "urn:xmpp:fallback:0" | ||||
|  | ||||
|  | ||||
| class Fallback(ElementBase): | ||||
|     namespace = NS | ||||
|     name = 'fallback' | ||||
|     plugin_attrib = 'fallback' | ||||
|     name = "fallback" | ||||
|     plugin_attrib = "fallback" | ||||
|     plugin_multi_attrib = "fallbacks" | ||||
|     interfaces = {"for"} | ||||
|  | ||||
|     def _find_fallback(self, fallback_for: str) -> "Fallback": | ||||
|         if self["for"] == fallback_for: | ||||
|             return self | ||||
|         for fallback in self.parent()["fallbacks"]: | ||||
|             if fallback["for"] == fallback_for: | ||||
|                 return fallback | ||||
|         raise AttributeError("No fallback for this namespace", fallback_for) | ||||
|  | ||||
|     def get_stripped_body( | ||||
|         self, fallback_for: str, element: Literal["body", "subject"] = "body" | ||||
|     ) -> str: | ||||
|         """ | ||||
|         Get the body of a message, with the fallback part stripped | ||||
|  | ||||
|         :param fallback_for: namespace of the fallback to strip | ||||
|         :param element: set this to "subject" get the stripped subject instead | ||||
|             of body | ||||
|  | ||||
|         :return: body (or subject) content minus the fallback part | ||||
|         """ | ||||
|         fallback = self._find_fallback(fallback_for) | ||||
|         start = fallback[element]["start"] | ||||
|         end = fallback[element]["end"] | ||||
|         body = self.parent()[element] | ||||
|         if start == end == 0: | ||||
|             return "" | ||||
|         if start <= end < len(body): | ||||
|             return body[:start] + body[end:] | ||||
|         else: | ||||
|             return body | ||||
|  | ||||
|  | ||||
| class FallbackMixin(ABC): | ||||
|     namespace = NS | ||||
|     name = NotImplemented | ||||
|     plugin_attrib = NotImplemented | ||||
|     interfaces = {"start", "end"} | ||||
|  | ||||
|     def set_start(self, v: int): | ||||
|         self._set_attr("start", str(v)) | ||||
|  | ||||
|     def get_start(self): | ||||
|         return _int_or_zero(self._get_attr("start")) | ||||
|  | ||||
|     def set_end(self, v: int): | ||||
|         self._set_attr("end", str(v)) | ||||
|  | ||||
|     def get_end(self): | ||||
|         return _int_or_zero(self._get_attr("end")) | ||||
|  | ||||
|  | ||||
| class FallbackBody(FallbackMixin, ElementBase): | ||||
|     name = plugin_attrib = "body" | ||||
|  | ||||
|  | ||||
| class FallbackSubject(FallbackMixin, ElementBase): | ||||
|     name = plugin_attrib = "subject" | ||||
|  | ||||
|  | ||||
| def _int_or_zero(v: str): | ||||
|     try: | ||||
|         return int(v) | ||||
|     except ValueError: | ||||
|         return 0 | ||||
|  | ||||
|  | ||||
| def register_plugins(): | ||||
|     register_stanza_plugin(Message, Fallback) | ||||
|     register_stanza_plugin(Message, Fallback, iterable=True) | ||||
|     register_stanza_plugin(Fallback, FallbackBody) | ||||
|     register_stanza_plugin(Fallback, FallbackSubject) | ||||
|   | ||||
| @@ -6,9 +6,7 @@ | ||||
| from typing import Set, Iterable | ||||
| from slixmpp.xmlstream import ElementBase | ||||
| try: | ||||
|     from emoji import UNICODE_EMOJI | ||||
|     if UNICODE_EMOJI.get('en'): | ||||
|         UNICODE_EMOJI = UNICODE_EMOJI['en'] | ||||
|     from emoji import EMOJI_DATA as UNICODE_EMOJI | ||||
| except ImportError: | ||||
|     UNICODE_EMOJI = None | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0446/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0446/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .file_metadata import XEP_0446 | ||||
|  | ||||
| register_plugin(XEP_0446) | ||||
							
								
								
									
										20
									
								
								slixmpp/plugins/xep_0446/file_metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								slixmpp/plugins/xep_0446/file_metadata.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import logging | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0446(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0446: File metadata element | ||||
|  | ||||
|     Minimum needed for xep 0447 (Stateless file sharing) | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0446" | ||||
|     description = "XEP-0446: File metadata element" | ||||
|     stanza = stanza | ||||
							
								
								
									
										38
									
								
								slixmpp/plugins/xep_0446/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								slixmpp/plugins/xep_0446/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from slixmpp.plugins.xep_0082 import format_datetime, parse | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NS = "urn:xmpp:file:metadata:0" | ||||
|  | ||||
|  | ||||
| class File(ElementBase): | ||||
|     name = "file" | ||||
|     namespace = NS | ||||
|     plugin_attrib = "file" | ||||
|     interfaces = sub_interfaces = {"media-type", "name", "date", "size", "hash", "desc"} | ||||
|  | ||||
|     def set_size(self, size: int): | ||||
|         self._set_sub_text("size", str(size)) | ||||
|  | ||||
|     def get_size(self): | ||||
|         return _int_or_none(self._get_sub_text("size")) | ||||
|  | ||||
|     def get_date(self): | ||||
|         try: | ||||
|             return parse(self._get_sub_text("date")) | ||||
|         except ValueError: | ||||
|             return | ||||
|  | ||||
|     def set_date(self, stamp: datetime): | ||||
|         try: | ||||
|             self._set_sub_text("date", format_datetime(stamp)) | ||||
|         except ValueError: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| def _int_or_none(v): | ||||
|     try: | ||||
|         return int(v) | ||||
|     except ValueError: | ||||
|         return None | ||||
							
								
								
									
										11
									
								
								slixmpp/plugins/xep_0447/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								slixmpp/plugins/xep_0447/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
|  | ||||
| # Slixmpp: The Slick XMPP Library | ||||
| # Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .sfs import XEP_0447 | ||||
|  | ||||
| register_plugin(XEP_0447) | ||||
							
								
								
									
										64
									
								
								slixmpp/plugins/xep_0447/sfs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								slixmpp/plugins/xep_0447/sfs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import logging | ||||
| from datetime import datetime | ||||
| from pathlib import Path | ||||
| from typing import Iterable, Optional | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class XEP_0447(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0447: Stateless File Sharing | ||||
|  | ||||
|     Only support outgoing SFS, incoming is not handled at all. | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0447" | ||||
|     description = "XEP-0447: Stateless File Sharing" | ||||
|     dependencies = {"xep_0300", "xep_0446"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         register_stanza_plugin(Message, stanza.StatelessFileSharing) | ||||
|  | ||||
|         register_stanza_plugin(stanza.StatelessFileSharing, stanza.Sources) | ||||
|         register_stanza_plugin( | ||||
|             stanza.StatelessFileSharing, self.xmpp["xep_0446"].stanza.File | ||||
|         ) | ||||
|         register_stanza_plugin(stanza.Sources, stanza.UrlData, iterable=True) | ||||
|  | ||||
|     def get_sfs( | ||||
|         self, | ||||
|         path: Path, | ||||
|         uris: Iterable[str], | ||||
|         media_type: Optional[str], | ||||
|         desc: Optional[str], | ||||
|     ): | ||||
|         sfs = stanza.StatelessFileSharing() | ||||
|         sfs["disposition"] = "inline" | ||||
|         for uri in uris: | ||||
|             ref = stanza.UrlData() | ||||
|             ref["target"] = uri | ||||
|             sfs["sources"].append(ref) | ||||
|         if media_type: | ||||
|             sfs["file"]["media-type"] = media_type | ||||
|         if desc: | ||||
|             sfs["file"]["desc"] = desc | ||||
|         sfs["file"]["name"] = path.name | ||||
|  | ||||
|         stat = path.stat() | ||||
|         sfs["file"]["size"] = stat.st_size | ||||
|         sfs["file"]["date"] = datetime.fromtimestamp(stat.st_mtime) | ||||
|  | ||||
|         h = self.xmpp.plugin["xep_0300"].compute_hash(path) | ||||
|         h["value"] = h["value"].decode() | ||||
|         sfs["file"].append(h) | ||||
|  | ||||
|         return sfs | ||||
							
								
								
									
										21
									
								
								slixmpp/plugins/xep_0447/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								slixmpp/plugins/xep_0447/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NAMESPACE = "urn:xmpp:sfs:0" | ||||
|  | ||||
|  | ||||
| class StatelessFileSharing(ElementBase): | ||||
|     name = "file-sharing" | ||||
|     plugin_attrib = "sfs" | ||||
|     namespace = NAMESPACE | ||||
|     interfaces = {"disposition"} | ||||
|  | ||||
|  | ||||
| class Sources(ElementBase): | ||||
|     name = plugin_attrib = "sources" | ||||
|     namespace = NAMESPACE | ||||
|  | ||||
|  | ||||
| class UrlData(ElementBase): | ||||
|     name = plugin_attrib = "url-data" | ||||
|     namespace = "http://jabber.org/protocol/url-data" | ||||
|     interfaces = {"target"} | ||||
							
								
								
									
										176
									
								
								slixmpp/plugins/xep_0454/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								slixmpp/plugins/xep_0454/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| # vim:fenc=utf-8 et ts=4 sts=4 sw=4 | ||||
| # | ||||
| # Copyright © 2022 Maxime “pep” Buquet <pep@bouah.net> | ||||
| # | ||||
| # See the LICENSE file for copying permissions. | ||||
|  | ||||
| """ | ||||
|     XEP-0454: OMEMO Media Sharing | ||||
| """ | ||||
|  | ||||
| from typing import IO, Optional, Tuple | ||||
|  | ||||
| from os import urandom | ||||
| from pathlib import Path | ||||
| from io import BytesIO, SEEK_END | ||||
|  | ||||
| from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | ||||
|  | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
|  | ||||
| class InvalidURL(Exception): | ||||
|     """Raised for URLs that either aren't HTTPS or already contain a fragment.""" | ||||
|  | ||||
|  | ||||
| EXTENSIONS_MAP = { | ||||
|     'jpeg': 'jpg', | ||||
|     'text': 'txt', | ||||
| } | ||||
|  | ||||
| class XEP_0454(BasePlugin): | ||||
|     """ | ||||
|         XEP-0454: OMEMO Media Sharing | ||||
|     """ | ||||
|  | ||||
|     name = 'xep_0454' | ||||
|     description = 'XEP-0454: OMEMO Media Sharing' | ||||
|     dependencies = {'xep_0363'} | ||||
|  | ||||
|     @staticmethod | ||||
|     def encrypt(input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]: | ||||
|         """ | ||||
|             Encrypts file as specified in XEP-0454 for use in file sharing | ||||
|  | ||||
|             :param input_file: Binary file stream on the file. | ||||
|             :param filename: Path to the file to upload. | ||||
|  | ||||
|             One of input_file or filename must be specified. If both are | ||||
|             passed, input_file will be used and filename ignored. | ||||
|         """ | ||||
|         if input_file is None and filename is None: | ||||
|             raise ValueError('Specify either filename or input_file parameter') | ||||
|  | ||||
|         aes_gcm_iv = urandom(12) | ||||
|         aes_gcm_key = urandom(32) | ||||
|  | ||||
|         aes_gcm = Cipher( | ||||
|             algorithms.AES(aes_gcm_key), | ||||
|             modes.GCM(aes_gcm_iv), | ||||
|         ).encryptor() | ||||
|  | ||||
|         if input_file is None: | ||||
|             input_file = open(filename, 'rb') | ||||
|  | ||||
|         payload = b'' | ||||
|         while True: | ||||
|             buf = input_file.read(4096) | ||||
|             if not buf: | ||||
|                 break | ||||
|             payload += aes_gcm.update(buf) | ||||
|  | ||||
|         payload += aes_gcm.finalize() + aes_gcm.tag | ||||
|         fragment = aes_gcm_iv.hex() + aes_gcm_key.hex() | ||||
|         return (payload, fragment) | ||||
|  | ||||
|     @staticmethod | ||||
|     def decrypt(input_file: IO[bytes], fragment: str) -> bytes: | ||||
|         """ | ||||
|             Decrypts file-like. | ||||
|  | ||||
|             :param input_file: Binary file stream on the file, containing the | ||||
|                                tag (16 bytes) at the end. | ||||
|             :param fragment: 88 hex chars string composed of iv (24 chars) | ||||
|                              + key (64 chars). | ||||
|         """ | ||||
|  | ||||
|         assert len(fragment) == 88 | ||||
|         aes_gcm_iv = bytes.fromhex(fragment[:24]) | ||||
|         aes_gcm_key = bytes.fromhex(fragment[24:]) | ||||
|  | ||||
|         # Find 16 bytes tag | ||||
|         input_file.seek(-16, SEEK_END) | ||||
|         tag = input_file.read() | ||||
|  | ||||
|         aes_gcm = Cipher( | ||||
|             algorithms.AES(aes_gcm_key), | ||||
|             modes.GCM(aes_gcm_iv, tag), | ||||
|         ).decryptor() | ||||
|  | ||||
|         size = input_file.seek(0, SEEK_END) | ||||
|         input_file.seek(0) | ||||
|  | ||||
|         count = size - 16 | ||||
|         plain = b'' | ||||
|         while count >= 0: | ||||
|             buf = input_file.read(4096) | ||||
|             count -= len(buf) | ||||
|             if count <= 0: | ||||
|                 buf += input_file.read() | ||||
|                 buf = buf[:-16] | ||||
|             plain += aes_gcm.update(buf) | ||||
|         plain += aes_gcm.finalize() | ||||
|  | ||||
|         return plain | ||||
|  | ||||
|     @staticmethod | ||||
|     def format_url(url: str, fragment: str) -> str: | ||||
|         """Helper to format a HTTPS URL to an AESGCM URI""" | ||||
|         if not url.startswith('https://') or url.find('#') != -1: | ||||
|             raise InvalidURL | ||||
|         return 'aesgcm://' + url[len('https://'):] + '#' + fragment | ||||
|  | ||||
|     @staticmethod | ||||
|     def map_extensions(ext: str) -> str: | ||||
|         """ | ||||
|             Apply conversions to extensions to reduce the number of | ||||
|             variations, (e.g., JPEG -> jpg). | ||||
|         """ | ||||
|         return EXTENSIONS_MAP.get(ext, ext).lower() | ||||
|  | ||||
|     async def upload_file( | ||||
|         self, | ||||
|         filename: Path, | ||||
|         _size: Optional[int] = None, | ||||
|         content_type: Optional[str] = None, | ||||
|         **kwargs, | ||||
|     ) -> str: | ||||
|         """ | ||||
|             Wrapper to xep_0363 (HTTP Upload)'s upload_file method. | ||||
|  | ||||
|             :param input_file: Binary file stream on the file. | ||||
|             :param filename: Path to the file to upload. | ||||
|  | ||||
|             Same as `XEP_0454.encrypt`, one of input_file or filename must be | ||||
|             specified. If both are passed, input_file will be used and | ||||
|             filename ignored. | ||||
|  | ||||
|             Other arguments passed in are passed to the actual | ||||
|             `XEP_0363.upload_file` call. | ||||
|         """ | ||||
|         input_file = kwargs.get('input_file') | ||||
|         payload, fragment = self.encrypt(input_file, filename) | ||||
|  | ||||
|         # Prepare kwargs for upload_file call | ||||
|         new_filename = urandom(12).hex()  # Random filename to hide user-provided path | ||||
|         if filename.suffix: | ||||
|             new_filename += self.map_extensions(filename.suffix) | ||||
|         kwargs['filename'] = new_filename | ||||
|  | ||||
|         input_enc = BytesIO(payload) | ||||
|         kwargs['input_file'] = input_enc | ||||
|  | ||||
|         # Size must also be overriden if provided | ||||
|         size = input_enc.seek(0, SEEK_END) | ||||
|         input_enc.seek(0) | ||||
|         kwargs['size'] = size | ||||
|  | ||||
|         kwargs['content_type'] = content_type | ||||
|  | ||||
|         url = await self.xmpp['xep_0363'].upload_file(**kwargs) | ||||
|         return self.format_url(url, fragment) | ||||
|  | ||||
| register_plugin(XEP_0454) | ||||
							
								
								
									
										6
									
								
								slixmpp/plugins/xep_0461/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								slixmpp/plugins/xep_0461/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from .reply import XEP_0461 | ||||
| from . import stanza | ||||
|  | ||||
| register_plugin(XEP_0461) | ||||
							
								
								
									
										48
									
								
								slixmpp/plugins/xep_0461/reply.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								slixmpp/plugins/xep_0461/reply.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from slixmpp.types import JidStr | ||||
| from slixmpp.xmlstream import StanzaBase | ||||
| from slixmpp.xmlstream.handler import Callback | ||||
| from slixmpp.xmlstream.matcher import StanzaPath | ||||
|  | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0461(BasePlugin): | ||||
|     """XEP-0461: Message Replies""" | ||||
|  | ||||
|     name = "xep_0461" | ||||
|     description = "XEP-0461: Message Replies" | ||||
|  | ||||
|     dependencies = {"xep_0030", "xep_0428"} | ||||
|     stanza = stanza | ||||
|     namespace = stanza.NS | ||||
|  | ||||
|     def plugin_init(self) -> None: | ||||
|         stanza.register_plugins() | ||||
|         self.xmpp.register_handler( | ||||
|             Callback( | ||||
|                 "Message replied to", | ||||
|                 StanzaPath("message/reply"), | ||||
|                 self._handle_reply_to_message, | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     def plugin_end(self): | ||||
|         self.xmpp.plugin["xep_0030"].del_feature(feature=stanza.NS) | ||||
|  | ||||
|     def session_bind(self, jid): | ||||
|         self.xmpp.plugin["xep_0030"].add_feature(feature=stanza.NS) | ||||
|  | ||||
|     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): | ||||
|         """ | ||||
|  | ||||
|         :param reply_to: Full JID of the quoted author | ||||
|         :param reply_id: ID of the message to reply to | ||||
|         """ | ||||
|         msg = self.xmpp.make_message(**msg_kwargs) | ||||
|         msg["reply"]["to"] = reply_to | ||||
|         msg["reply"]["id"] = reply_id | ||||
|         msg.send() | ||||
							
								
								
									
										56
									
								
								slixmpp/plugins/xep_0461/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								slixmpp/plugins/xep_0461/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp.stanza import Message | ||||
| from slixmpp.xmlstream import ElementBase, register_stanza_plugin | ||||
| from slixmpp.plugins.xep_0428.stanza import Fallback | ||||
|  | ||||
| NS = "urn:xmpp:reply:0" | ||||
|  | ||||
|  | ||||
| class Reply(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "reply" | ||||
|     plugin_attrib = "reply" | ||||
|     interfaces = {"id", "to"} | ||||
|  | ||||
|     def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None): | ||||
|         """ | ||||
|         Add plain text fallback for clients not implementing XEP-0461. | ||||
|  | ||||
|         ``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will | ||||
|         prepend "> Bob:\n> Some text\n" to the body of the message, and set the | ||||
|         fallback_body attributes accordingly, so that clients implementing | ||||
|         XEP-0461 can hide the fallback text. | ||||
|  | ||||
|         :param fallback: Body of the quoted message. | ||||
|         :param nickname: Optional, nickname of the quoted participant. | ||||
|         """ | ||||
|         msg = self.parent() | ||||
|         quoted = "\n".join("> " + x.strip() for x in fallback.split("\n")) + "\n" | ||||
|         if nickname: | ||||
|             quoted = "> " + nickname + ":\n" + quoted | ||||
|         msg["body"] = quoted + msg["body"] | ||||
|         fallback = Fallback() | ||||
|         fallback["for"] = NS | ||||
|         fallback["body"]["start"] = 0 | ||||
|         fallback["body"]["end"] = len(quoted) | ||||
|         msg.append(fallback) | ||||
|  | ||||
|     def get_fallback_body(self) -> str: | ||||
|         msg = self.parent() | ||||
|         for fallback in msg["fallbacks"]: | ||||
|             if fallback["for"] == NS: | ||||
|                 break | ||||
|         else: | ||||
|             return "" | ||||
|         start = fallback["body"]["start"] | ||||
|         end = fallback["body"]["end"] | ||||
|         body = msg["body"] | ||||
|         if start <= end: | ||||
|             return body[start:end] | ||||
|         else: | ||||
|             return "" | ||||
|  | ||||
|  | ||||
| def register_plugins(): | ||||
|     register_stanza_plugin(Message, Reply) | ||||
							
								
								
									
										8
									
								
								slixmpp/plugins/xep_0469/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								slixmpp/plugins/xep_0469/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| from slixmpp.plugins.base import register_plugin | ||||
|  | ||||
| from . import stanza | ||||
| from .pinning import XEP_0469 | ||||
|  | ||||
| register_plugin(XEP_0469) | ||||
|  | ||||
| __all__ = ['stanza', 'XEP_0469'] | ||||
							
								
								
									
										17
									
								
								slixmpp/plugins/xep_0469/pinning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								slixmpp/plugins/xep_0469/pinning.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| from slixmpp.plugins import BasePlugin | ||||
| from . import stanza | ||||
|  | ||||
|  | ||||
| class XEP_0469(BasePlugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0469: Bookmark Pinning | ||||
|     """ | ||||
|  | ||||
|     name = "xep_0469" | ||||
|     description = "XEP-0469: Bookmark Pinning" | ||||
|     dependencies = {"xep_0402"} | ||||
|     stanza = stanza | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         stanza.register_plugin() | ||||
							
								
								
									
										31
									
								
								slixmpp/plugins/xep_0469/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								slixmpp/plugins/xep_0469/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| from slixmpp import register_stanza_plugin | ||||
| from slixmpp.plugins.xep_0402.stanza import Extensions | ||||
| from slixmpp.xmlstream import ElementBase | ||||
|  | ||||
| NS = "urn:xmpp:bookmarks-pinning:0" | ||||
|  | ||||
|  | ||||
| class Pinned(ElementBase): | ||||
|     """ | ||||
|     Pinned bookmark element | ||||
|  | ||||
|  | ||||
|     To enable it on a Conference element, use enable() like this: | ||||
|  | ||||
|     .. code-block::python | ||||
|  | ||||
|         # C being a Conference element | ||||
|         C['extensions'].enable('pinned') | ||||
|  | ||||
|     Which will add the <pinned> element to the <extensions> element. | ||||
|     """ | ||||
|     namespace = NS | ||||
|     name = "pinned" | ||||
|     plugin_attrib = "pinned" | ||||
|     interfaces = {"pinned"} | ||||
|     bool_interfaces = {"pinned"} | ||||
|     is_extension = True | ||||
|  | ||||
|  | ||||
| def register_plugin(): | ||||
|     register_stanza_plugin(Extensions, Pinned) | ||||
| @@ -69,12 +69,14 @@ from slixmpp.plugins.xep_0249 import XEP_0249 | ||||
| from slixmpp.plugins.xep_0256 import XEP_0256 | ||||
| from slixmpp.plugins.xep_0257 import XEP_0257 | ||||
| from slixmpp.plugins.xep_0258 import XEP_0258 | ||||
| from slixmpp.plugins.xep_0264 import XEP_0264 | ||||
| from slixmpp.plugins.xep_0279 import XEP_0279 | ||||
| from slixmpp.plugins.xep_0280 import XEP_0280 | ||||
| from slixmpp.plugins.xep_0297 import XEP_0297 | ||||
| from slixmpp.plugins.xep_0300 import XEP_0300 | ||||
| from slixmpp.plugins.xep_0308 import XEP_0308 | ||||
| from slixmpp.plugins.xep_0313 import XEP_0313 | ||||
| from slixmpp.plugins.xep_0317 import XEP_0317 | ||||
| from slixmpp.plugins.xep_0319 import XEP_0319 | ||||
| from slixmpp.plugins.xep_0332 import XEP_0332 | ||||
| from slixmpp.plugins.xep_0333 import XEP_0333 | ||||
| @@ -100,6 +102,7 @@ from slixmpp.plugins.xep_0428 import XEP_0428 | ||||
| from slixmpp.plugins.xep_0437 import XEP_0437 | ||||
| from slixmpp.plugins.xep_0439 import XEP_0439 | ||||
| from slixmpp.plugins.xep_0444 import XEP_0444 | ||||
| from slixmpp.plugins.xep_0461 import XEP_0461 | ||||
|  | ||||
|  | ||||
| class PluginsDict(TypedDict): | ||||
| @@ -162,12 +165,14 @@ class PluginsDict(TypedDict): | ||||
|     xep_0256: XEP_0256 | ||||
|     xep_0257: XEP_0257 | ||||
|     xep_0258: XEP_0258 | ||||
|     xep_0264: XEP_0264 | ||||
|     xep_0279: XEP_0279 | ||||
|     xep_0280: XEP_0280 | ||||
|     xep_0297: XEP_0297 | ||||
|     xep_0300: XEP_0300 | ||||
|     xep_0308: XEP_0308 | ||||
|     xep_0313: XEP_0313 | ||||
|     xep_0317: XEP_0317 | ||||
|     xep_0319: XEP_0319 | ||||
|     xep_0332: XEP_0332 | ||||
|     xep_0333: XEP_0333 | ||||
| @@ -193,3 +198,4 @@ class PluginsDict(TypedDict): | ||||
|     xep_0437: XEP_0437 | ||||
|     xep_0439: XEP_0439 | ||||
|     xep_0444: XEP_0444 | ||||
|     xep_0461: XEP_0461 | ||||
|   | ||||
| @@ -64,9 +64,9 @@ class Message(RootStanza): | ||||
|             if self.stream: | ||||
|                 use_ids = getattr(self.stream, 'use_message_ids', None) | ||||
|                 if use_ids: | ||||
|                     self['id'] = self.stream.new_id() | ||||
|                     self.set_id(self.stream.new_id()) | ||||
|             else: | ||||
|                 del self['origin_id'] | ||||
|                 self.del_origin_id() | ||||
|  | ||||
|     def get_type(self): | ||||
|         """ | ||||
| @@ -96,8 +96,8 @@ class Message(RootStanza): | ||||
|         self.xml.attrib['id'] = value | ||||
|  | ||||
|         if self.stream: | ||||
|             use_orig_ids = getattr(self.stream, 'use_origin_id', None) | ||||
|             if not use_orig_ids: | ||||
|             if not getattr(self.stream, 'use_origin_id', False): | ||||
|                 self.del_origin_id() | ||||
|                 return None | ||||
|  | ||||
|         sub = self.xml.find(ORIGIN_NAME) | ||||
| @@ -176,7 +176,7 @@ class Message(RootStanza): | ||||
|         """ | ||||
|         new_message = StanzaBase.reply(self, clear) | ||||
|  | ||||
|         if self['type'] == 'groupchat': | ||||
|         if not getattr(self.stream, "is_component", False) and self['type'] == 'groupchat': | ||||
|             new_message['to'] = new_message['to'].bare | ||||
|  | ||||
|         new_message['thread'] = self['thread'] | ||||
|   | ||||
| @@ -63,6 +63,8 @@ class RootStanza(StanzaBase): | ||||
|             reply['error']['condition'] = e.condition | ||||
|             reply['error']['text'] = e.text | ||||
|             reply['error']['type'] = e.etype | ||||
|             if e.by: | ||||
|                 reply["error"]["by"] = e.by | ||||
|             if e.extension is not None: | ||||
|                 # Extended error tag | ||||
|                 extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), | ||||
|   | ||||
| @@ -29,9 +29,9 @@ class SlixIntegration(IsolatedAsyncioTestCase): | ||||
|         self.clients = [] | ||||
|         self.addAsyncCleanup(self._destroy) | ||||
|  | ||||
|     def envjid(self, name): | ||||
|     def envjid(self, name: str, *, default: Optional[str] = None) -> JID: | ||||
|         """Get a JID from an env var""" | ||||
|         value = os.getenv(name) | ||||
|         value = os.getenv(name, default=default) | ||||
|         return JID(value) | ||||
|  | ||||
|     def envstr(self, name): | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| import atexit | ||||
| import unittest | ||||
| from queue import Queue | ||||
| from xml.parsers.expat import ExpatError | ||||
| @@ -10,11 +11,13 @@ from xml.parsers.expat import ExpatError | ||||
| from slixmpp.test import TestTransport | ||||
| from slixmpp import ClientXMPP, ComponentXMPP | ||||
| from slixmpp.stanza import Message, Iq, Presence | ||||
| from slixmpp.stanza.error import Error | ||||
| from slixmpp.xmlstream import ET | ||||
| from slixmpp.xmlstream import ElementBase | ||||
| from slixmpp.xmlstream.tostring import tostring, highlight | ||||
| from slixmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender | ||||
| from slixmpp.xmlstream.matcher import MatchXMLMask, MatchXPath | ||||
| from slixmpp.xmlstream.stanzabase import register_stanza_plugin | ||||
|  | ||||
| import asyncio | ||||
|  | ||||
| @@ -322,6 +325,7 @@ class SlixTest(unittest.TestCase): | ||||
|         if not plugin_config: | ||||
|             plugin_config = {} | ||||
|  | ||||
|         self.mode = mode | ||||
|         if mode == 'client': | ||||
|             self.xmpp = ClientXMPP(jid, password, | ||||
|                                    sasl_mech=sasl_mech, | ||||
| @@ -740,3 +744,19 @@ class SlixTest(unittest.TestCase): | ||||
|  | ||||
|         # Everything matches | ||||
|         return True | ||||
|  | ||||
|     def tearDown(self): | ||||
|         self.stream_close() | ||||
|         if getattr(self, "mode", None) == "component": | ||||
|             Error.namespace = 'jabber:client' | ||||
|             for st in Message, Iq, Presence: | ||||
|                 register_stanza_plugin(st, Error) | ||||
|  | ||||
|  | ||||
| @atexit.register | ||||
| def cleanup(): | ||||
|     try: | ||||
|         loop = asyncio.get_event_loop() | ||||
|         loop.close() | ||||
|     except: | ||||
|         pass | ||||
|   | ||||
							
								
								
									
										1
									
								
								slixmpp/thirdparty/__init__.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								slixmpp/thirdparty/__init__.py
									
									
									
									
										vendored
									
									
								
							| @@ -3,5 +3,4 @@ try: | ||||
| except: | ||||
|     from slixmpp.thirdparty.gnupg import GPG | ||||
|  | ||||
| from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso | ||||
| from slixmpp.thirdparty.orderedset import OrderedSet | ||||
|   | ||||
							
								
								
									
										273
									
								
								slixmpp/thirdparty/mini_dateutil.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										273
									
								
								slixmpp/thirdparty/mini_dateutil.py
									
									
									
									
										vendored
									
									
								
							| @@ -1,273 +0,0 @@ | ||||
| # This module is a very stripped down version of the dateutil | ||||
| # package for when dateutil has not been installed. As a replacement | ||||
| # for dateutil.parser.parse, the parsing methods from | ||||
| # http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/ | ||||
|  | ||||
| #As such, the following copyrights and licenses applies: | ||||
|  | ||||
|  | ||||
| # dateutil - Extensions to the standard python 2.3+ datetime module. | ||||
| # | ||||
| # Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net> | ||||
| # | ||||
| # All rights reserved. | ||||
| # | ||||
| # Redistribution and use in source and binary forms, with or without | ||||
| # modification, are permitted provided that the following conditions are met: | ||||
| # | ||||
| #     * Redistributions of source code must retain the above copyright notice, | ||||
| #       this list of conditions and the following disclaimer. | ||||
| #     * Redistributions in binary form must reproduce the above copyright notice, | ||||
| #       this list of conditions and the following disclaimer in the documentation | ||||
| #       and/or other materials provided with the distribution. | ||||
| #     * Neither the name of the copyright holder nor the names of its | ||||
| #       contributors may be used to endorse or promote products derived from | ||||
| #       this software without specific prior written permission. | ||||
| # | ||||
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | ||||
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | ||||
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | ||||
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | ||||
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | ||||
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | ||||
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
|  | ||||
| # fixed_dateime | ||||
| # | ||||
| # Copyright (c) 2008, Red Innovation Ltd., Finland | ||||
| # All rights reserved. | ||||
| # | ||||
| # Redistribution and use in source and binary forms, with or without | ||||
| # modification, are permitted provided that the following conditions are met: | ||||
| #     * Redistributions of source code must retain the above copyright | ||||
| #       notice, this list of conditions and the following disclaimer. | ||||
| #     * Redistributions in binary form must reproduce the above copyright | ||||
| #       notice, this list of conditions and the following disclaimer in the | ||||
| #       documentation and/or other materials provided with the distribution. | ||||
| #     * Neither the name of Red Innovation nor the names of its contributors | ||||
| #       may be used to endorse or promote products derived from this software | ||||
| #       without specific prior written permission. | ||||
| # | ||||
| # THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY | ||||
| # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| # DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY | ||||
| # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||
| # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||
| # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||||
| # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
|  | ||||
|  | ||||
| import re | ||||
| import math | ||||
| import datetime | ||||
|  | ||||
|  | ||||
| ZERO = datetime.timedelta(0) | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from dateutil.parser import parse as parse_iso | ||||
|     from dateutil.tz import tzoffset, tzutc | ||||
| except: | ||||
|     # As a stopgap, define the two timezones here based | ||||
|     # on the dateutil code. | ||||
|  | ||||
|     class tzutc(datetime.tzinfo): | ||||
|  | ||||
|         def utcoffset(self, dt): | ||||
|             return ZERO | ||||
|  | ||||
|         def dst(self, dt): | ||||
|             return ZERO | ||||
|  | ||||
|         def tzname(self, dt): | ||||
|             return "UTC" | ||||
|  | ||||
|         def __eq__(self, other): | ||||
|             return (isinstance(other, tzutc) or | ||||
|                     (isinstance(other, tzoffset) and other._offset == ZERO)) | ||||
|  | ||||
|         def __ne__(self, other): | ||||
|             return not self.__eq__(other) | ||||
|  | ||||
|         def __repr__(self): | ||||
|             return "%s()" % self.__class__.__name__ | ||||
|  | ||||
|         __reduce__ = object.__reduce__ | ||||
|  | ||||
|     class tzoffset(datetime.tzinfo): | ||||
|  | ||||
|         def __init__(self, name, offset): | ||||
|             self._name = name | ||||
|             self._offset = datetime.timedelta(minutes=offset) | ||||
|  | ||||
|         def utcoffset(self, dt): | ||||
|             return self._offset | ||||
|  | ||||
|         def dst(self, dt): | ||||
|             return ZERO | ||||
|  | ||||
|         def tzname(self, dt): | ||||
|             return self._name | ||||
|  | ||||
|         def __eq__(self, other): | ||||
|             return (isinstance(other, tzoffset) and | ||||
|                     self._offset == other._offset) | ||||
|  | ||||
|         def __ne__(self, other): | ||||
|             return not self.__eq__(other) | ||||
|  | ||||
|         def __repr__(self): | ||||
|             return "%s(%s, %s)" % (self.__class__.__name__, | ||||
|                                    repr(self._name), | ||||
|                                    self._offset.days*86400+self._offset.seconds) | ||||
|  | ||||
|         __reduce__ = object.__reduce__ | ||||
|  | ||||
|  | ||||
|     _fixed_offset_tzs = { } | ||||
|     UTC = tzutc() | ||||
|  | ||||
|     def _get_fixed_offset_tz(offsetmins): | ||||
|         """For internal use only: Returns a tzinfo with | ||||
|         the given fixed offset. This creates only one instance | ||||
|         for each offset; the zones are kept in a dictionary""" | ||||
|  | ||||
|         if offsetmins == 0: | ||||
|             return UTC | ||||
|  | ||||
|         if not offsetmins in _fixed_offset_tzs: | ||||
|             if offsetmins < 0: | ||||
|                 sign = '-' | ||||
|                 absoff = -offsetmins | ||||
|             else: | ||||
|                 sign = '+' | ||||
|                 absoff = offsetmins | ||||
|  | ||||
|             name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) | ||||
|             inst = tzoffset(name,offsetmins) | ||||
|             _fixed_offset_tzs[offsetmins] = inst | ||||
|  | ||||
|         return _fixed_offset_tzs[offsetmins] | ||||
|  | ||||
|  | ||||
|     _iso8601_parser = re.compile(r""" | ||||
|         ^ | ||||
|         (?P<year> [0-9]{4})?(?P<ymdsep>-?)? | ||||
|         (?P<month>[0-9]{2})?(?P=ymdsep)? | ||||
|         (?P<day>  [0-9]{2})? | ||||
|  | ||||
|         (?P<time> | ||||
|             (?: # time part... optional... at least hour must be specified | ||||
|             (?:T|\s+)? | ||||
|                 (?P<hour>[0-9]{2}) | ||||
|                 (?: | ||||
|                     # minutes, separated with :, or none, from hours | ||||
|                     (?P<hmssep>[:]?) | ||||
|                     (?P<minute>[0-9]{2}) | ||||
|                     (?: | ||||
|                         # same for seconds, separated with :, or none, from hours | ||||
|                         (?P=hmssep) | ||||
|                         (?P<second>[0-9]{2}) | ||||
|                     )? | ||||
|                 )? | ||||
|  | ||||
|                 # fractions | ||||
|                 (?: [,.] (?P<frac>[0-9]{1,10}))? | ||||
|  | ||||
|                 # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there. | ||||
|                 ( | ||||
|                     (?P<tzempty>Z) | ||||
|                 | | ||||
|                     (?P<tzh>[+-][0-9]{2}) | ||||
|                     (?: :? # optional separator | ||||
|                         (?P<tzm>[0-9]{2}) | ||||
|                     )? | ||||
|                 )? | ||||
|             ) | ||||
|         )? | ||||
|         $ | ||||
|     """, re.X) # """ | ||||
|  | ||||
|     def parse_iso(timestamp): | ||||
|         """Internal function for parsing a timestamp in | ||||
|         ISO 8601 format""" | ||||
|  | ||||
|         timestamp = timestamp.strip() | ||||
|  | ||||
|         m = _iso8601_parser.match(timestamp) | ||||
|         if not m: | ||||
|             raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp) | ||||
|  | ||||
|         vals = m.groupdict() | ||||
|         def_vals = {'year': 1970, 'month': 1, 'day': 1} | ||||
|         for key in vals: | ||||
|             if vals[key] is None: | ||||
|                 vals[key] = def_vals.get(key, 0) | ||||
|             elif key not in ['time', 'ymdsep', 'hmssep', 'tzempty']: | ||||
|                 vals[key] = int(vals[key]) | ||||
|  | ||||
|         year  = vals['year'] | ||||
|         month = vals['month'] | ||||
|         day   = vals['day'] | ||||
|  | ||||
|         if m.group('time') is None: | ||||
|             return datetime.date(year, month, day) | ||||
|  | ||||
|         h, min, s, us = None, None, None, 0 | ||||
|         frac = 0 | ||||
|         if m.group('tzempty') == None and m.group('tzh') == None: | ||||
|             raise ValueError("Not a proper ISO 8601 timestamp: " + | ||||
|                     "missing timezone (Z or +hh[:mm])!") | ||||
|  | ||||
|         if m.group('frac'): | ||||
|             frac = m.group('frac') | ||||
|             power = len(frac) | ||||
|             frac  = int(frac) / 10.0 ** power | ||||
|  | ||||
|         if m.group('hour'): | ||||
|             h = vals['hour'] | ||||
|  | ||||
|         if m.group('minute'): | ||||
|             min = vals['minute'] | ||||
|  | ||||
|         if m.group('second'): | ||||
|             s = vals['second'] | ||||
|  | ||||
|         if frac != None: | ||||
|             # ok, fractions of hour? | ||||
|             if min == None: | ||||
|                 frac, min = math.modf(frac * 60.0) | ||||
|                 min = int(min) | ||||
|  | ||||
|             # fractions of second? | ||||
|             if s == None: | ||||
|                 frac, s = math.modf(frac * 60.0) | ||||
|                 s = int(s) | ||||
|  | ||||
|             # and extract microseconds... | ||||
|             us = int(frac * 1000000) | ||||
|  | ||||
|         if m.group('tzempty') == 'Z': | ||||
|             offsetmins = 0 | ||||
|         else: | ||||
|             # timezone: hour diff with sign | ||||
|             offsetmins = vals['tzh'] * 60 | ||||
|             tzm = m.group('tzm') | ||||
|  | ||||
|             # add optional minutes | ||||
|             if tzm != None: | ||||
|                 tzm = int(tzm) | ||||
|                 offsetmins += tzm if offsetmins > 0 else -tzm | ||||
|  | ||||
|         tz = _get_fixed_offset_tz(offsetmins) | ||||
|         return datetime.datetime(year, month, day, h, min, s, us, tz) | ||||
| @@ -83,8 +83,35 @@ MAMDefault = Literal['always', 'never', 'roster'] | ||||
|  | ||||
| FilterString = Literal['in', 'out', 'out_sync'] | ||||
|  | ||||
| __all__ = [ | ||||
|     'Protocol', 'TypedDict', 'Literal', 'OptJid', 'JidStr', 'MAMDefault', | ||||
|     'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole', | ||||
|     'MucAffiliation', 'FilterString', | ||||
| ErrorTypes = Literal["modify", "cancel", "auth", "wait", "cancel"] | ||||
|  | ||||
| ErrorConditions = Literal[ | ||||
|     "bad-request", | ||||
|     "conflict", | ||||
|     "feature-not-implemented", | ||||
|     "forbidden", | ||||
|     "gone", | ||||
|     "internal-server-error", | ||||
|     "item-not-found", | ||||
|     "jid-malformed", | ||||
|     "not-acceptable", | ||||
|     "not-allowed", | ||||
|     "not-authorized", | ||||
|     "payment-required", | ||||
|     "recipient-unavailable", | ||||
|     "redirect", | ||||
|     "registration-required", | ||||
|     "remote-server-not-found", | ||||
|     "remote-server-timeout", | ||||
|     "resource-constraint", | ||||
|     "service-unavailable", | ||||
|     "subscription-required", | ||||
|     "undefined-condition", | ||||
|     "unexpected-request", | ||||
| ] | ||||
|  | ||||
| __all__ = [ | ||||
|     'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault', | ||||
|     'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole', | ||||
|     'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes' | ||||
| ] | ||||
|   | ||||
| @@ -181,7 +181,7 @@ class SCRAM(Mech): | ||||
|     channel_binding = True | ||||
|     required_credentials = {'username', 'password'} | ||||
|     optional_credentials = {'authzid', 'channel_binding'} | ||||
|     security = {'encrypted', 'unencrypted_scram'} | ||||
|     security = {'tls_version', 'encrypted', 'unencrypted_scram', 'binding_proposed'} | ||||
|  | ||||
|     def setup(self, name): | ||||
|         self.use_channel_binding = False | ||||
| @@ -244,11 +244,15 @@ class SCRAM(Mech): | ||||
|         self.cnonce = bytes(('%s' % random.random())[2:]) | ||||
|  | ||||
|         gs2_cbind_flag = b'n' | ||||
|         if self.credentials['channel_binding']: | ||||
|             if self.use_channel_binding: | ||||
|                 gs2_cbind_flag = b'p=tls-unique' | ||||
|             else: | ||||
|                 gs2_cbind_flag = b'y' | ||||
|         if self.security_settings['binding_proposed']: | ||||
|             if self.credentials['channel_binding'] and \ | ||||
|                     self.use_channel_binding: | ||||
|                 if self.security_settings['tls_version'] != 'TLSv1.3': | ||||
|                     gs2_cbind_flag = b'p=tls-unique' | ||||
|                 else: | ||||
|                     gs2_cbind_flag = b'p=tls-exporter' | ||||
|         else: | ||||
|             gs2_cbind_flag = b'y' | ||||
|  | ||||
|         authzid = b'' | ||||
|         if self.credentials['authzid']: | ||||
| @@ -280,7 +284,7 @@ class SCRAM(Mech): | ||||
|             raise SASLCancelled('Invalid nonce') | ||||
|  | ||||
|         cbind_data = b'' | ||||
|         if self.use_channel_binding: | ||||
|         if self.use_channel_binding and self.credentials['channel_binding']: | ||||
|             cbind_data = self.credentials['channel_binding'] | ||||
|         cbind_input = self.gs2_header + cbind_data | ||||
|         channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') | ||||
|   | ||||
| @@ -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.0' | ||||
| __version_info__ = (1, 8, 0) | ||||
| __version__ = '1.8.5' | ||||
| __version_info__ = (1, 8, 5) | ||||
|   | ||||
| @@ -10,5 +10,5 @@ from slixmpp.xmlstream.tostring import tostring, highlight | ||||
| from slixmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT | ||||
|  | ||||
| __all__ = ['JID', 'StanzaBase', 'ElementBase', | ||||
|            'ET', 'StateMachine', 'tostring', 'highlight', 'XMLStream', | ||||
|            'RESPONSE_TIMEOUT'] | ||||
|            'ET', 'tostring', 'highlight', 'XMLStream', | ||||
|            'RESPONSE_TIMEOUT', 'register_stanza_plugin'] | ||||
|   | ||||
| @@ -15,7 +15,13 @@ from slixmpp.types import Protocol | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class AnswerProtocol(Protocol): | ||||
| class GetHostByNameAnswerProtocol(Protocol): | ||||
|     name: str | ||||
|     aliases: List[str] | ||||
|     addresses: List[str] | ||||
|  | ||||
|  | ||||
| class QueryAnswerProtocol(Protocol): | ||||
|     host: str | ||||
|     priority: int | ||||
|     weight: int | ||||
| @@ -23,6 +29,9 @@ class AnswerProtocol(Protocol): | ||||
|  | ||||
|  | ||||
| class ResolverProtocol(Protocol): | ||||
|     def gethostbyname(self, host: str, socket_family: socket.AddressFamily) -> Future: | ||||
|         ... | ||||
|  | ||||
|     def query(self, query: str, querytype: str) -> Future: | ||||
|         ... | ||||
|  | ||||
| @@ -147,11 +156,6 @@ async def resolve(host: str, port: int, *, loop: AbstractEventLoop, | ||||
|  | ||||
|     results = [] | ||||
|     for host, port in hosts: | ||||
|         if host == 'localhost': | ||||
|             if use_ipv6: | ||||
|                 results.append((host, '::1', port)) | ||||
|             results.append((host, '127.0.0.1', port)) | ||||
|  | ||||
|         if use_ipv6: | ||||
|             aaaa = await get_AAAA(host, resolver=resolver, | ||||
|                                        use_aiodns=use_aiodns, loop=loop) | ||||
| @@ -201,13 +205,13 @@ async def get_A(host: str, *, loop: AbstractEventLoop, | ||||
|             return [] | ||||
|  | ||||
|     # Using aiodns: | ||||
|     future = resolver.query(host, 'A') | ||||
|     future = resolver.gethostbyname(host, socket.AF_INET) | ||||
|     try: | ||||
|         recs = cast(Iterable[AnswerProtocol], await future) | ||||
|         recs = cast(GetHostByNameAnswerProtocol, await future) | ||||
|     except Exception as e: | ||||
|         log.debug('DNS: Exception while querying for %s A records: %s', host, e) | ||||
|         recs = [] | ||||
|     return [rec.host for rec in recs] | ||||
|         return [] | ||||
|     return [addr for addr in recs.addresses] | ||||
|  | ||||
|  | ||||
| async def get_AAAA(host: str, *, loop: AbstractEventLoop, | ||||
| @@ -249,13 +253,13 @@ async def get_AAAA(host: str, *, loop: AbstractEventLoop, | ||||
|             return [] | ||||
|  | ||||
|     # Using aiodns: | ||||
|     future = resolver.query(host, 'AAAA') | ||||
|     future = resolver.gethostbyname(host, socket.AF_INET6) | ||||
|     try: | ||||
|         recs = cast(Iterable[AnswerProtocol], await future) | ||||
|         recs = cast(GetHostByNameAnswerProtocol, await future) | ||||
|     except Exception as e: | ||||
|         log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e) | ||||
|         recs = [] | ||||
|     return [rec.host for rec in recs] | ||||
|         return [] | ||||
|     return [addr for addr in recs.addresses] | ||||
|  | ||||
|  | ||||
| async def get_SRV(host: str, port: int, service: str, | ||||
| @@ -295,12 +299,12 @@ async def get_SRV(host: str, port: int, service: str, | ||||
|     try: | ||||
|         future = resolver.query('_%s._%s.%s' % (service, proto, host), | ||||
|                                 'SRV') | ||||
|         recs = cast(Iterable[AnswerProtocol], await future) | ||||
|         recs = cast(Iterable[QueryAnswerProtocol], await future) | ||||
|     except Exception as e: | ||||
|         log.debug('DNS: Exception while querying for %s SRV records: %s', host, e) | ||||
|         return [] | ||||
|  | ||||
|     answers: Dict[int, List[AnswerProtocol]] = {} | ||||
|     answers: Dict[int, List[QueryAnswerProtocol]] = {} | ||||
|     for rec in recs: | ||||
|         if rec.priority not in answers: | ||||
|             answers[rec.priority] = [] | ||||
|   | ||||
| @@ -1243,7 +1243,7 @@ class ElementBase(object): | ||||
|                 self.init_plugin(item.__class__.plugin_multi_attrib) | ||||
|         else: | ||||
|             self.iterables.append(item) | ||||
|  | ||||
|         item.parent = weakref.ref(self) | ||||
|         return self | ||||
|  | ||||
|     def appendxml(self, xml: ET.Element) -> ElementBase: | ||||
|   | ||||
| @@ -35,6 +35,7 @@ import ssl | ||||
| import uuid | ||||
| import warnings | ||||
| import weakref | ||||
| import collections | ||||
|  | ||||
| from contextlib import contextmanager | ||||
| import xml.etree.ElementTree as ET | ||||
| @@ -82,7 +83,7 @@ class InvalidCABundle(Exception): | ||||
|         Exception raised when the CA Bundle file hasn't been found. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, path: Optional[Path]): | ||||
|     def __init__(self, path: Optional[Union[Path, Iterable[Path]]]): | ||||
|         self.path = path | ||||
|  | ||||
|  | ||||
| @@ -289,8 +290,8 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         self.xml_depth = 0 | ||||
|         self.xml_root = None | ||||
|  | ||||
|         self.force_starttls = None | ||||
|         self.disable_starttls = None | ||||
|         self.force_starttls = True | ||||
|         self.disable_starttls = False | ||||
|  | ||||
|         self.waiting_queue = asyncio.Queue() | ||||
|  | ||||
| @@ -298,8 +299,8 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         self.scheduled_events = {} | ||||
|  | ||||
|         self.ssl_context = ssl.create_default_context() | ||||
|         self.ssl_context.check_hostname = False | ||||
|         self.ssl_context.verify_mode = ssl.CERT_NONE | ||||
|         self.ssl_context.check_hostname = True | ||||
|         self.ssl_context.verify_mode = ssl.CERT_REQUIRED | ||||
|  | ||||
|         self.event_when_connected = "connected" | ||||
|  | ||||
| @@ -404,8 +405,9 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             self.disconnected.set_result(True) | ||||
|         self.disconnected = asyncio.Future() | ||||
|  | ||||
|     def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = False, | ||||
|                 force_starttls: Optional[bool] = True, disable_starttls: Optional[bool] = False) -> None: | ||||
|     def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None, | ||||
|                 force_starttls: Optional[bool] = None, | ||||
|                 disable_starttls: Optional[bool] = None) -> None: | ||||
|         """Create a new socket and connect to the server. | ||||
|  | ||||
|         :param host: The name of the desired server for the connection. | ||||
| @@ -483,25 +485,21 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         if self._current_connection_attempt is None: | ||||
|             return | ||||
|         try: | ||||
|             server_hostname = self.default_domain if self.use_ssl else None | ||||
|             await self.loop.create_connection(lambda: self, | ||||
|                                                    self.address[0], | ||||
|                                                    self.address[1], | ||||
|                                                    ssl=ssl_context, | ||||
|                                                    server_hostname=self.default_domain if self.use_ssl else None) | ||||
|                                                    server_hostname=server_hostname) | ||||
|             self._connect_loop_wait = 0 | ||||
|         except Socket.gaierror as e: | ||||
|             self.event('connection_failed', | ||||
|                        'No DNS record available for %s' % self.default_domain) | ||||
|             self.reschedule_connection_attempt() | ||||
|         except OSError as e: | ||||
|             log.debug('Connection failed: %s', e) | ||||
|             self.event("connection_failed", e) | ||||
|             if self._current_connection_attempt is None: | ||||
|                 return | ||||
|             self._connect_loop_wait = self._connect_loop_wait * 2 + 1 | ||||
|             self._current_connection_attempt = asyncio.ensure_future( | ||||
|                 self._connect_routine(), | ||||
|                 loop=self.loop, | ||||
|             ) | ||||
|             self.reschedule_connection_attempt() | ||||
|  | ||||
|     def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None: | ||||
|         """Process all the available XMPP events (receiving or sending data on the | ||||
| @@ -526,7 +524,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             else: | ||||
|                 self.loop.run_until_complete(self.disconnected) | ||||
|         else: | ||||
|             tasks: List[Future] = [asyncio.sleep(timeout)] | ||||
|             tasks: List[Union[asyncio.Task, asyncio.Future]] = [asyncio.Task(asyncio.sleep(timeout))] | ||||
|             if not forever: | ||||
|                 tasks.append(self.disconnected) | ||||
|             self.loop.run_until_complete(asyncio.wait(tasks)) | ||||
| @@ -578,7 +576,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                                                        stream=self, | ||||
|                                                        top_level=True, | ||||
|                                                        open_only=True)) | ||||
|                         self.start_stream_handler(self.xml_root) | ||||
|                         self.start_stream_handler(self.xml_root)  # type:ignore | ||||
|                     self.xml_depth += 1 | ||||
|                 if event == 'end': | ||||
|                     self.xml_depth -= 1 | ||||
| @@ -637,6 +635,20 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         self._set_disconnected_future() | ||||
|         self.event("disconnected", self.disconnect_reason or exception) | ||||
|  | ||||
|     def reschedule_connection_attempt(self) -> None: | ||||
|         """ | ||||
|         Increase the exponential back-off and initate another background | ||||
|         _connect_routine call to connect to the server. | ||||
|         """ | ||||
|         # abort if there is no ongoing connection attempt | ||||
|         if self._current_connection_attempt is None: | ||||
|             return | ||||
|         self._connect_loop_wait = min(300, self._connect_loop_wait * 2 + 1) | ||||
|         self._current_connection_attempt = asyncio.ensure_future( | ||||
|             self._connect_routine(), | ||||
|             loop=self.loop, | ||||
|         ) | ||||
|  | ||||
|     def cancel_connection_attempt(self) -> None: | ||||
|         """ | ||||
|         Immediately cancel the current create_connection() Future. | ||||
| @@ -793,11 +805,14 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                     if bundle.is_file(): | ||||
|                         ca_cert = bundle | ||||
|                         break | ||||
|             if ca_cert is None: | ||||
|                 raise InvalidCABundle(ca_cert) | ||||
|             if ca_cert is None and \ | ||||
|                isinstance(self.ca_certs, (Path, collections.abc.Iterable)): | ||||
|                 raise InvalidCABundle(self.ca_certs) | ||||
|  | ||||
|             self.ssl_context.verify_mode = ssl.CERT_REQUIRED | ||||
|             self.ssl_context.load_verify_locations(cafile=ca_cert) | ||||
|         else: | ||||
|             self.ssl_context.set_default_verify_paths() | ||||
|  | ||||
|         return self.ssl_context | ||||
|  | ||||
| @@ -814,15 +829,15 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         try: | ||||
|             if hasattr(self.loop, 'start_tls'): | ||||
|                 transp = await self.loop.start_tls(self.transport, | ||||
|                                                    self, ssl_context) | ||||
|                                                    self, ssl_context, | ||||
|                                                    server_hostname=self.default_domain) | ||||
|             # Python < 3.7 | ||||
|             else: | ||||
|                 transp, _ = await self.loop.create_connection( | ||||
|                     lambda: self, | ||||
|                     ssl=self.ssl_context, | ||||
|                     sock=self.socket, | ||||
|                     server_hostname=self.default_domain | ||||
|                 ) | ||||
|                     server_hostname=self.default_domain) | ||||
|         except ssl.SSLError as e: | ||||
|             log.debug('SSL: Unable to connect', exc_info=True) | ||||
|             log.error('CERT: Invalid certificate trust chain.') | ||||
| @@ -835,6 +850,8 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             log.debug("Connection error:", exc_info=True) | ||||
|             self.disconnect() | ||||
|             return False | ||||
|         if transp is None: | ||||
|             raise Exception("Transport should not be none") | ||||
|         der_cert = transp.get_extra_info("ssl_object").getpeercert(True) | ||||
|         pem_cert = ssl.DER_cert_to_PEM_cert(der_cert) | ||||
|         self.event('ssl_cert', pem_cert) | ||||
| @@ -1254,7 +1271,7 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|                             already_run_filters.add(filter) | ||||
|                             if iscoroutinefunction(filter): | ||||
|                                 filter = cast(AsyncFilter, filter) | ||||
|                                 task = asyncio.create_task(filter(data)) | ||||
|                                 task = asyncio.create_task(filter(data))  # type:ignore | ||||
|                                 completed, pending = await wait( | ||||
|                                     {task}, | ||||
|                                     timeout=1, | ||||
| @@ -1318,10 +1335,18 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             # Avoid circular imports | ||||
|             from slixmpp.stanza.rootstanza import RootStanza | ||||
|             from slixmpp.stanza import Iq, Handshake | ||||
|             passthrough = ( | ||||
|                 (isinstance(data, Iq) and data.get_plugin('bind', check=True)) | ||||
|                 or isinstance(data, Handshake) | ||||
|             ) | ||||
|  | ||||
|             passthrough = False | ||||
|             if isinstance(data, Iq): | ||||
|                 if data.get_plugin('bind', check=True): | ||||
|                     passthrough = True | ||||
|                 elif data.get_plugin('session', check=True): | ||||
|                     passthrough = True | ||||
|                 elif data.get_plugin('register', check=True): | ||||
|                     passthrough = True | ||||
|             elif isinstance(data, Handshake): | ||||
|                 passthrough = True | ||||
|  | ||||
|             if isinstance(data, (RootStanza, str)) and not passthrough: | ||||
|                 self.__queued_stanzas.append((data, use_filters)) | ||||
|                 log.debug('NOT SENT: %s %s', type(data), data) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user