Compare commits
	
		
			22 Commits
		
	
	
		
			pyproject-
			...
			fix-channe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 51cbe87501 | ||
|   | ef02b3a596 | ||
|   | c25305e80f | ||
|   | 6765f84133 | ||
|   | 31fe7f7e06 | ||
|   | 84a7ac020f | ||
|   | 331c1c1e21 | ||
|   | 28a60c22e2 | ||
|   | af934b5bdf | ||
|   | 897f876504 | ||
|   | 2888be17ab | ||
|   | 975e31229c | ||
|   | 6e9e66139d | ||
|   | 380ac04d52 | ||
|   | 9e5b530607 | ||
|   | 71de274fab | ||
|   | 5a0b02378d | ||
|   | 9fc82e9e6f | ||
|   | ca90d3908e | ||
|   | 7de5cbcf33 | ||
|   | 76a11d4899 | ||
|   | dcfa0f20f9 | 
							
								
								
									
										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 | ||||
							
								
								
									
										7
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| language: python | ||||
| python: | ||||
|   - "3.7" | ||||
|   - "3.8-dev" | ||||
| install: | ||||
|   - "pip install ." | ||||
| script: testall.py | ||||
							
								
								
									
										6
									
								
								.woodpecker/lint.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.woodpecker/lint.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| steps: | ||||
|   mypy: | ||||
|     image: python:3 | ||||
|     script: | ||||
|       - pip3 install mypy types-setuptools | ||||
|       - mypy slixmpp | ||||
							
								
								
									
										9
									
								
								.woodpecker/test-integration.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.woodpecker/test-integration.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| steps: | ||||
|   test_integration: | ||||
|     image: "python:3.11" | ||||
|     secrets: [ci_account1, ci_account1_password, ci_account2, ci_account2_password] | ||||
|     commands: | ||||
|       - apt-get update | ||||
|       - apt-get install -y python3-pip cython3 gpg | ||||
|       - pip3 install emoji aiohttp aiodns | ||||
|       - ./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" | ||||
| @@ -1,50 +0,0 @@ | ||||
| """ | ||||
| Cythonize the .pyx file for sdist, and compile it for wheels. | ||||
| NB: produced wheels are not valid, but the sdist should be. | ||||
| """ | ||||
|  | ||||
| import os | ||||
| from Cython.Build import cythonize | ||||
| from subprocess import check_output, CalledProcessError, DEVNULL, call | ||||
| from tempfile import TemporaryFile | ||||
|  | ||||
| from setuptools.command.build_py import build_py | ||||
|  | ||||
|  | ||||
| def check_include(library_name, header): | ||||
|     command = [os.environ.get("PKG_CONFIG", "pkg-config"), "--cflags", library_name] | ||||
|     try: | ||||
|         cflags = check_output(command).decode("utf-8").split() | ||||
|     except FileNotFoundError: | ||||
|         print("pkg-config not found.") | ||||
|         return False | ||||
|     except CalledProcessError: | ||||
|         # pkg-config already prints the missing libraries on stderr. | ||||
|         return False | ||||
|     command = [os.environ.get("CC", "cc")] + cflags + ["-E", "-"] | ||||
|     with TemporaryFile("w+") as c_file: | ||||
|         c_file.write("#include <%s>" % header) | ||||
|         c_file.seek(0) | ||||
|         try: | ||||
|             return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0 | ||||
|         except FileNotFoundError: | ||||
|             print("%s headers not found." % library_name) | ||||
|             return False | ||||
|  | ||||
|  | ||||
| class Build(build_py): | ||||
|     def run(self): | ||||
|         self.run_command("build_ext") | ||||
|         return super().run() | ||||
|  | ||||
|     def initialize_options(self): | ||||
|         super().initialize_options() | ||||
|  | ||||
|         has_python_headers = check_include("python3", "Python.h") | ||||
|         has_stringprep_headers = check_include("libidn", "stringprep.h") | ||||
|  | ||||
|         if has_python_headers and has_stringprep_headers: | ||||
|             self.distribution.ext_modules = cythonize("slixmpp/stringprep.pyx") | ||||
|  | ||||
|         else: | ||||
|             print("Falling back to the slow stringprep module.") | ||||
							
								
								
									
										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>`_ | ||||
							
								
								
									
										15
									
								
								mypy.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mypy.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| [mypy] | ||||
| check_untyped_defs = False | ||||
| ignore_missing_imports = True | ||||
|  | ||||
| [mypy-slixmpp.types] | ||||
| ignore_errors = True | ||||
|  | ||||
| [mypy-slixmpp.thirdparty.*] | ||||
| ignore_errors = True | ||||
|  | ||||
| [mypy-slixmpp.plugins.*] | ||||
| ignore_errors = True | ||||
|  | ||||
| [mypy-slixmpp.plugins.base] | ||||
| ignore_errors = False | ||||
| @@ -1,67 +0,0 @@ | ||||
| [project] | ||||
| name = "slixmpp" | ||||
| version = "1.8.4" | ||||
| description = 'Slixmpp is an elegant Python library for XMPP (aka Jabber).' | ||||
| requires-python = ">=3.7" | ||||
| dependencies = [ | ||||
|     "aiodns >= 1.0", | ||||
|     "pyasn1", | ||||
|     "pyasn1_modules", | ||||
|     "typing_extensions; python_version < '3.8.0'", | ||||
| ] | ||||
| classifiers = [ | ||||
|     'Intended Audience :: Developers', | ||||
|     'License :: OSI Approved :: MIT License', | ||||
|     'Programming Language :: Python', | ||||
|     'Programming Language :: Python :: 3.7', | ||||
|     'Programming Language :: Python :: 3.8', | ||||
|     'Programming Language :: Python :: 3.9', | ||||
|     'Topic :: Internet :: XMPP', | ||||
|     'Topic :: Software Development :: Libraries :: Python Modules', | ||||
| ] | ||||
| readme = "README.rst" | ||||
| license = { file = "LICENSE" } | ||||
|  | ||||
| [[project.authors]] | ||||
| name = "Florent Le Coz" | ||||
| email = "louiz@louiz.org" | ||||
|  | ||||
| [project.urls] | ||||
| Repository = 'https://codeberg.org/poezio/slixmpp' | ||||
|  | ||||
| [project.optional-dependencies] | ||||
| XEP-0363 = ['aiohttp'] | ||||
| XEP-0444 = ['emoji'] | ||||
| XEP-0454 = ['cryptography'] | ||||
| Safer-XML-parsing = ['defusedxml'] | ||||
|  | ||||
| [build-system] | ||||
| requires = ["setuptools", "cython"] | ||||
| build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [tool.setuptools] | ||||
| packages = ["slixmpp"] | ||||
| py-modules = ["_custom_build"] | ||||
|  | ||||
| [tool.setuptools.cmdclass] | ||||
| build_py = "_custom_build.Build" | ||||
|  | ||||
| [tool.mypy] | ||||
| check_untyped_defs = false | ||||
| ignore_missing_imports = true | ||||
|  | ||||
| [[tool.mypy.overrides]] | ||||
| module = 'slixmpp.types' | ||||
| ignore_errors = true | ||||
|  | ||||
| [[tool.mypy.overrides]] | ||||
| module = 'slixmpp.thirdparty.*' | ||||
| ignore_errors = true | ||||
|  | ||||
| [[tool.mypy.overrides]] | ||||
| module = 'slixmpp.plugins.*' | ||||
| ignore_errors = true | ||||
|  | ||||
| [[tool.mypy.overrides]] | ||||
| module = 'slixmpp.plugins.base' | ||||
| ignore_errors = false | ||||
							
								
								
									
										103
									
								
								setup.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										103
									
								
								setup.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| # | ||||
| # Copyright (C) 2007-2011 Nathanael C. Fritz | ||||
| # All Rights Reserved | ||||
| # | ||||
| # This software is licensed as described in the README.rst and LICENSE | ||||
| # file, which you should have received as part of this distribution. | ||||
|  | ||||
| import runpy | ||||
| import os | ||||
| from pathlib import Path | ||||
| from subprocess import call, DEVNULL, check_output, CalledProcessError | ||||
| from tempfile import TemporaryFile | ||||
| try: | ||||
|     from setuptools import setup | ||||
| except ImportError: | ||||
|     from distutils.core import setup | ||||
|  | ||||
| from run_tests import TestCommand | ||||
|  | ||||
| version_mod = runpy.run_path('slixmpp/version.py') | ||||
| VERSION = version_mod['__version__'] | ||||
|  | ||||
| DESCRIPTION = ('Slixmpp is an elegant Python library for XMPP (aka Jabber).') | ||||
| with open('README.rst', encoding='utf8') as readme: | ||||
|     LONG_DESCRIPTION = readme.read() | ||||
|  | ||||
| CLASSIFIERS = [ | ||||
|     'Intended Audience :: Developers', | ||||
|     'License :: OSI Approved :: MIT License', | ||||
|     'Programming Language :: Python', | ||||
|     'Programming Language :: Python :: 3.7', | ||||
|     'Programming Language :: Python :: 3.8', | ||||
|     'Programming Language :: Python :: 3.9', | ||||
|     'Topic :: Internet :: XMPP', | ||||
|     'Topic :: Software Development :: Libraries :: Python Modules', | ||||
| ] | ||||
|  | ||||
| packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')] | ||||
|  | ||||
| def check_include(library_name, header): | ||||
|     command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name] | ||||
|     try: | ||||
|         cflags = check_output(command).decode('utf-8').split() | ||||
|     except FileNotFoundError: | ||||
|         print('pkg-config not found.') | ||||
|         return False | ||||
|     except CalledProcessError: | ||||
|         # pkg-config already prints the missing libraries on stderr. | ||||
|         return False | ||||
|     command = [os.environ.get('CC', 'cc')] + cflags + ['-E', '-'] | ||||
|     with TemporaryFile('w+') as c_file: | ||||
|         c_file.write('#include <%s>' % header) | ||||
|         c_file.seek(0) | ||||
|         try: | ||||
|             return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0 | ||||
|         except FileNotFoundError: | ||||
|             print('%s headers not found.' % library_name) | ||||
|             return False | ||||
|  | ||||
| HAS_PYTHON_HEADERS = check_include('python3', 'Python.h') | ||||
| HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h') | ||||
|  | ||||
| ext_modules = None | ||||
| if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS: | ||||
|     try: | ||||
|         from Cython.Build import cythonize | ||||
|     except ImportError: | ||||
|         print('Cython not found, falling back to the slow stringprep module.') | ||||
|     else: | ||||
|         ext_modules = cythonize('slixmpp/stringprep.pyx') | ||||
| else: | ||||
|     print('Falling back to the slow stringprep module.') | ||||
|  | ||||
| setup( | ||||
|     name="slixmpp", | ||||
|     version=VERSION, | ||||
|     description=DESCRIPTION, | ||||
|     long_description=LONG_DESCRIPTION, | ||||
|     author='Florent Le Coz', | ||||
|     author_email='louiz@louiz.org', | ||||
|     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"', | ||||
|     ], | ||||
|     extras_require={ | ||||
|         'XEP-0363': ['aiohttp'], | ||||
|         'XEP-0444 compliance': ['emoji'], | ||||
|         'XEP-0454': ['cryptography'], | ||||
|         'Safer XML parsing': ['defusedxml'], | ||||
|     }, | ||||
|     classifiers=CLASSIFIERS, | ||||
|     cmdclass={'test': TestCommand} | ||||
| ) | ||||
| @@ -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,6 +9,8 @@ | ||||
| import logging | ||||
| import hashlib | ||||
|  | ||||
| from typing import Optional | ||||
|  | ||||
| from slixmpp import Message, Iq, Presence | ||||
| from slixmpp.basexmpp import BaseXMPP | ||||
| from slixmpp.stanza import Handshake | ||||
| @@ -93,7 +95,7 @@ class ComponentXMPP(BaseXMPP): | ||||
|         for st in Message, Iq, Presence: | ||||
|             register_stanza_plugin(st, Error) | ||||
|  | ||||
|     def connect(self, host=None, port=None, use_ssl=False): | ||||
|     def connect(self, host: str = None, port: int = 0, use_ssl: Optional[bool] = None) -> None: | ||||
|         """Connect to the server. | ||||
|  | ||||
|  | ||||
| @@ -104,16 +106,15 @@ class ComponentXMPP(BaseXMPP): | ||||
|         :param use_ssl: Flag indicating if SSL should be used by connecting | ||||
|                         directly to a port using SSL. | ||||
|         """ | ||||
|         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): | ||||
|         """ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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:2" | ||||
|     namespace = NS | ||||
|     name = "privilege" | ||||
|     plugin_attrib = "privilege" | ||||
|  | ||||
| @@ -25,26 +24,40 @@ class Privilege(ElementBase): | ||||
|     def presence(self): | ||||
|         return self.permission("presence") | ||||
|  | ||||
|     def iq(self): | ||||
|         return self.permission("iq") | ||||
|  | ||||
|     def add_perm(self, access, type): | ||||
|     def add_perm(self, access, type_): | ||||
|         # This should only be needed for servers, so maybe out of scope for slixmpp | ||||
|         perm = Perm() | ||||
|         perm["type"] = type | ||||
|         perm["type"] = type_ | ||||
|         perm["access"] = access | ||||
|         self.append(perm) | ||||
|  | ||||
|  | ||||
| class Perm(ElementBase): | ||||
|     namespace = "urn:xmpp:privilege:2" | ||||
|     namespace = NS | ||||
|     name = "perm" | ||||
|     plugin_attrib = "perm" | ||||
|     plugin_multi_attrib = "perms" | ||||
|     interfaces = {"type", "access"} | ||||
|  | ||||
|  | ||||
| class NameSpace(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "namespace" | ||||
|     plugin_attrib = "namespace" | ||||
|     plugin_multi_attrib = "namespaces" | ||||
|     interfaces = {"ns", "type"} | ||||
|  | ||||
|  | ||||
| class PrivilegedIq(ElementBase): | ||||
|     namespace = NS | ||||
|     name = "privileged_iq" | ||||
|     plugin_attrib = "privileged_iq" | ||||
|  | ||||
|  | ||||
| def register(): | ||||
|     register_stanza_plugin(Message, Privilege) | ||||
|     register_stanza_plugin(Iq, Privilege) | ||||
|     register_stanza_plugin(Privilege, Forwarded) | ||||
|     register_stanza_plugin(Privilege, Perm, iterable=True) | ||||
|     register_stanza_plugin(Perm, NameSpace, iterable=True) | ||||
|     register_stanza_plugin(Iq, PrivilegedIq) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class XEP_0461(BasePlugin): | ||||
|     name = "xep_0461" | ||||
|     description = "XEP-0461: Message Replies" | ||||
|  | ||||
|     dependencies = {"xep_0030"} | ||||
|     dependencies = {"xep_0030", "xep_0428"} | ||||
|     stanza = stanza | ||||
|     namespace = stanza.NS | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ 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" | ||||
|  | ||||
| @@ -12,39 +13,11 @@ class Reply(ElementBase): | ||||
|     plugin_attrib = "reply" | ||||
|     interfaces = {"id", "to"} | ||||
|  | ||||
|  | ||||
| class FeatureFallBack(ElementBase): | ||||
|     # should also be a multi attrib | ||||
|     namespace = "urn:xmpp:fallback:0" | ||||
|     name = "fallback" | ||||
|     plugin_attrib = "feature_fallback" | ||||
|     interfaces = {"for"} | ||||
|  | ||||
|     def get_fallback_body(self): | ||||
|         # only works for a single fallback_body attrib | ||||
|         start = self["fallback_body"]["start"] | ||||
|         end = self["fallback_body"]["end"] | ||||
|         body = self.parent()["body"] | ||||
|         if start <= end: | ||||
|             return body[start:end] | ||||
|         else: | ||||
|             return "" | ||||
|  | ||||
|     def get_stripped_body(self): | ||||
|         # only works for a single fallback_body attrib | ||||
|         start = self["fallback_body"]["start"] | ||||
|         end = self["fallback_body"]["end"] | ||||
|         body = self.parent()["body"] | ||||
|         if start <= end < len(body): | ||||
|             return body[:start] + body[end:] | ||||
|         else: | ||||
|             return body | ||||
|  | ||||
|     def add_quoted_fallback(self, fallback: str, nickname: Optional[str] = None): | ||||
|         """ | ||||
|         Add plain text fallback for clients not implementing XEP-0461. | ||||
|  | ||||
|         ``msg["feature_fallback"].add_quoted_fallback("Some text", "Bob")`` will | ||||
|         ``msg["reply"].add_quoted_fallback("Some text", "Bob")`` will | ||||
|         prepend "> Bob:\n> Some text\n" to the body of the message, and set the | ||||
|         fallback_body attributes accordingly, so that clients implementing | ||||
|         XEP-0461 can hide the fallback text. | ||||
| @@ -57,39 +30,27 @@ class FeatureFallBack(ElementBase): | ||||
|         if nickname: | ||||
|             quoted = "> " + nickname + ":\n" + quoted | ||||
|         msg["body"] = quoted + msg["body"] | ||||
|         msg["feature_fallback"]["for"] = NS | ||||
|         msg["feature_fallback"]["fallback_body"]["start"] = 0 | ||||
|         msg["feature_fallback"]["fallback_body"]["end"] = len(quoted) | ||||
|         fallback = Fallback() | ||||
|         fallback["for"] = NS | ||||
|         fallback["body"]["start"] = 0 | ||||
|         fallback["body"]["end"] = len(quoted) | ||||
|         msg.append(fallback) | ||||
|  | ||||
|  | ||||
| class FallBackBody(ElementBase): | ||||
|     # According to https://xmpp.org/extensions/inbox/compatibility-fallback.html | ||||
|     # this should be a multi_attrib *but* since it's a protoXEP, we'll see... | ||||
|     namespace = FeatureFallBack.namespace | ||||
|     name = "body" | ||||
|     plugin_attrib = "fallback_body" | ||||
|     interfaces = {"start", "end"} | ||||
|  | ||||
|     def set_start(self, v: int): | ||||
|         self._set_attr("start", str(v)) | ||||
|  | ||||
|     def get_start(self): | ||||
|         try: | ||||
|             return int(self._get_attr("start")) | ||||
|         except ValueError: | ||||
|             return 0 | ||||
|  | ||||
|     def set_end(self, v: int): | ||||
|         self._set_attr("end", str(v)) | ||||
|  | ||||
|     def get_end(self): | ||||
|         try: | ||||
|             return int(self._get_attr("end")) | ||||
|         except ValueError: | ||||
|             return 0 | ||||
|     def 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) | ||||
|     register_stanza_plugin(Message, FeatureFallBack) | ||||
|     register_stanza_plugin(FeatureFallBack, FallBackBody) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout | ||||
| # This file is part of Slixmpp. | ||||
| # See the file LICENSE for copying permission. | ||||
| import atexit | ||||
| import unittest | ||||
| from queue import Queue | ||||
| from xml.parsers.expat import ExpatError | ||||
| @@ -750,3 +751,9 @@ class SlixTest(unittest.TestCase): | ||||
|             Error.namespace = 'jabber:client' | ||||
|             for st in Message, Iq, Presence: | ||||
|                 register_stanza_plugin(st, Error) | ||||
|  | ||||
|  | ||||
| @atexit.register | ||||
| def cleanup(): | ||||
|     loop = asyncio.get_event_loop() | ||||
|     loop.close() | ||||
|   | ||||
| @@ -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'') | ||||
|   | ||||
| @@ -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'] | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -290,8 +290,8 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|         self.xml_depth = 0 | ||||
|         self.xml_root = None | ||||
|  | ||||
|         self.force_starttls = None | ||||
|         self.disable_starttls = None | ||||
|         self.force_starttls = True | ||||
|         self.disable_starttls = False | ||||
|  | ||||
|         self.waiting_queue = asyncio.Queue() | ||||
|  | ||||
| @@ -405,8 +405,9 @@ class XMLStream(asyncio.BaseProtocol): | ||||
|             self.disconnected.set_result(True) | ||||
|         self.disconnected = asyncio.Future() | ||||
|  | ||||
|     def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = False, | ||||
|                 force_starttls: Optional[bool] = True, disable_starttls: Optional[bool] = False) -> None: | ||||
|     def connect(self, host: str = '', port: int = 0, use_ssl: Optional[bool] = None, | ||||
|                 force_starttls: Optional[bool] = None, | ||||
|                 disable_starttls: Optional[bool] = None) -> None: | ||||
|         """Create a new socket and connect to the server. | ||||
|  | ||||
|         :param host: The name of the desired server for the connection. | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| import unittest | ||||
| from slixmpp import Message | ||||
| from slixmpp.test import SlixTest | ||||
| from slixmpp.xmlstream import register_stanza_plugin | ||||
|  | ||||
| from slixmpp.plugins.xep_0356 import stanza | ||||
| from slixmpp.plugins.xep_0356 import stanza, permissions | ||||
|  | ||||
|  | ||||
| class TestPermissions(SlixTest): | ||||
| @@ -12,30 +10,57 @@ class TestPermissions(SlixTest): | ||||
|  | ||||
|     def testAdvertisePermission(self): | ||||
|         xmlstring = """ | ||||
|             <message from='capulet.net' to='pubub.capulet.lit'> | ||||
|             <message from='capulet.lit' to='pubsub.capulet.lit'> | ||||
|                 <privilege xmlns='urn:xmpp:privilege:2'> | ||||
|                     <perm access='roster' type='both'/> | ||||
|                     <perm access='message' type='outgoing'/> | ||||
|                     <perm access='presence' type='managed_entity'/> | ||||
|                     <perm access='iq' type='both'/> | ||||
|                 </privilege> | ||||
|             </message> | ||||
|         """ | ||||
|         msg = self.Message() | ||||
|         msg["from"] = "capulet.net" | ||||
|         msg["to"] = "pubub.capulet.lit" | ||||
|         # This raises AttributeError: 'NoneType' object has no attribute 'use_origin_id' | ||||
|         # msg["id"] = "id" | ||||
|         msg["from"] = "capulet.lit" | ||||
|         msg["to"] = "pubsub.capulet.lit" | ||||
|  | ||||
|         for access, type_ in [ | ||||
|             ("roster", "both"), | ||||
|             ("message", "outgoing"), | ||||
|             ("presence", "managed_entity"), | ||||
|             ("roster", permissions.RosterAccess.BOTH), | ||||
|             ("message", permissions.MessagePermission.OUTGOING), | ||||
|             ("presence", permissions.PresencePermission.MANAGED_ENTITY), | ||||
|             ("iq", permissions.IqPermission.BOTH), | ||||
|         ]: | ||||
|             msg["privilege"].add_perm(access, type_) | ||||
|  | ||||
|         self.check(msg, xmlstring) | ||||
|         # Should this one work? →        # AttributeError: 'Message' object has no attribute 'permission' | ||||
|         # self.assertEqual(msg.permission["roster"], "both") | ||||
|  | ||||
|     def testIqPermission(self): | ||||
|         x = stanza.Privilege() | ||||
|         x["access"] = "iq" | ||||
|         ns = stanza.NameSpace() | ||||
|         ns["ns"] = "some_ns" | ||||
|         ns["type"] = "get" | ||||
|         x["perm"]["access"] = "iq" | ||||
|         x["perm"].append(ns) | ||||
|         ns = stanza.NameSpace() | ||||
|         ns["ns"] = "some_other_ns" | ||||
|         ns["type"] = "both" | ||||
|         x["perm"].append(ns) | ||||
|         self.check( | ||||
|             x, | ||||
|             """ | ||||
|               <privilege xmlns='urn:xmpp:privilege:2'> | ||||
|                 <perm access='iq'> | ||||
|                   <namespace ns='some_ns' type='get' /> | ||||
|                   <namespace ns='some_other_ns' type='both' /> | ||||
|                 </perm> | ||||
|               </privilege> | ||||
|             """ | ||||
|         ) | ||||
|         nss = set() | ||||
|         for perm in x["perms"]: | ||||
|             for ns in perm["namespaces"]: | ||||
|                 nss.add((ns["ns"], ns["type"])) | ||||
|         assert nss == {("some_ns", "get"), ("some_other_ns", "both")} | ||||
|  | ||||
|  | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestPermissions) | ||||
|   | ||||
							
								
								
									
										149
									
								
								tests/test_stanza_xep_0428.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								tests/test_stanza_xep_0428.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| import unittest | ||||
|  | ||||
| from slixmpp import Message | ||||
| from slixmpp.test import SlixTest | ||||
| from slixmpp.plugins.xep_0428 import stanza | ||||
|  | ||||
| from slixmpp.plugins import xep_0461 | ||||
| from slixmpp.plugins import xep_0444 | ||||
|  | ||||
|  | ||||
| class TestFallback(SlixTest): | ||||
|     def setUp(self): | ||||
|         stanza.register_plugins() | ||||
|  | ||||
|     def testSingleFallbackBody(self): | ||||
|         message = Message() | ||||
|         message["fallback"]["for"] = "ns" | ||||
|         message["fallback"]["body"]["start"] = 0 | ||||
|         message["fallback"]["body"]["end"] = 8 | ||||
|  | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns'> | ||||
|                 <body start="0" end="8" /> | ||||
|               </fallback> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|     def testSingleFallbackSubject(self): | ||||
|         message = Message() | ||||
|         message["fallback"]["for"] = "ns" | ||||
|         message["fallback"]["subject"]["start"] = 0 | ||||
|         message["fallback"]["subject"]["end"] = 8 | ||||
|  | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns'> | ||||
|                 <subject start="0" end="8" /> | ||||
|               </fallback> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|     def testSingleFallbackWholeBody(self): | ||||
|         message = Message() | ||||
|         message["fallback"]["for"] = "ns" | ||||
|         message["fallback"].enable("body") | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns'> | ||||
|                 <body /> | ||||
|               </fallback> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|     def testMultiFallback(self): | ||||
|         message = Message() | ||||
|  | ||||
|         f1 = stanza.Fallback() | ||||
|         f1["for"] = "ns1" | ||||
|  | ||||
|         f2 = stanza.Fallback() | ||||
|         f2["for"] = "ns2" | ||||
|  | ||||
|         message.append(f1) | ||||
|         message.append(f2) | ||||
|  | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns1' /> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns2' /> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|         for i, fallback in enumerate(message["fallbacks"], start=1): | ||||
|             self.assertEqual(fallback["for"], f"ns{i}") | ||||
|  | ||||
|     def testStripFallbackPartOfBody(self): | ||||
|         message = Message() | ||||
|         message["body"] = "> quoted\nsome-body" | ||||
|         message["fallback"]["for"] = xep_0461.stanza.NS | ||||
|         message["fallback"]["body"]["start"] = 0 | ||||
|         message["fallback"]["body"]["end"] = 9 | ||||
|  | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <body>> quoted\nsome-body</body> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:reply:0'> | ||||
|                 <body start="0" end="9" /> | ||||
|               </fallback> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual( | ||||
|             message["fallback"].get_stripped_body(xep_0461.stanza.NS), "some-body" | ||||
|         ) | ||||
|  | ||||
|     def testStripWholeBody(self): | ||||
|         message = Message() | ||||
|         message["body"] = "> quoted\nsome-body" | ||||
|         message["fallback"]["for"] = "ns" | ||||
|         message["fallback"].enable("body") | ||||
|  | ||||
|         self.check( | ||||
|             message,  # language=XML | ||||
|             """ | ||||
|             <message> | ||||
|               <body>> quoted\nsome-body</body> | ||||
|               <fallback xmlns='urn:xmpp:fallback:0' for='ns'> | ||||
|                 <body /> | ||||
|               </fallback> | ||||
|             </message> | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|         self.assertEqual(message["fallback"].get_stripped_body("ns"), "") | ||||
|  | ||||
|     def testStripMultiFallback(self): | ||||
|         message = Message() | ||||
|         message["body"] = "> huuuuu\n👍" | ||||
|  | ||||
|         message["fallback"]["for"] = xep_0461.stanza.NS | ||||
|         message["fallback"]["body"]["start"] = 0 | ||||
|         message["fallback"]["body"]["end"] = 9 | ||||
|  | ||||
|         reaction_fallback = stanza.Fallback() | ||||
|         reaction_fallback["for"] = xep_0444.stanza.NS | ||||
|         reaction_fallback.enable("body") | ||||
|         message.append(reaction_fallback) | ||||
|  | ||||
|         self.assertEqual(message["fallback"].get_stripped_body(xep_0461.stanza.NS), "👍") | ||||
|         self.assertEqual(message["fallback"].get_stripped_body(xep_0444.stanza.NS), "") | ||||
|  | ||||
|  | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestFallback) | ||||
| @@ -1,11 +1,13 @@ | ||||
| import unittest | ||||
| from slixmpp import Message | ||||
| from slixmpp.test import SlixTest | ||||
| from slixmpp.plugins.xep_0428 import stanza as fallback_stanza | ||||
| from slixmpp.plugins.xep_0461 import stanza | ||||
|  | ||||
|  | ||||
| class TestReply(SlixTest): | ||||
|     def setUp(self): | ||||
|         fallback_stanza.register_plugins() | ||||
|         stanza.register_plugins() | ||||
|  | ||||
|     def testReply(self): | ||||
| @@ -26,9 +28,9 @@ class TestReply(SlixTest): | ||||
|     def testFallback(self): | ||||
|         message = Message() | ||||
|         message["body"] = "12345\nrealbody" | ||||
|         message["feature_fallback"]["for"] = "NS" | ||||
|         message["feature_fallback"]["fallback_body"]["start"] = 0 | ||||
|         message["feature_fallback"]["fallback_body"]["end"] = 6 | ||||
|         message["fallback"]["for"] = "NS" | ||||
|         message["fallback"]["body"]["start"] = 0 | ||||
|         message["fallback"]["body"]["end"] = 6 | ||||
|  | ||||
|         self.check( | ||||
|             message, | ||||
| @@ -42,18 +44,18 @@ class TestReply(SlixTest): | ||||
|             """, | ||||
|         ) | ||||
|  | ||||
|         assert message["feature_fallback"].get_stripped_body() == "realbody" | ||||
|         assert message["fallback"].get_stripped_body("NS") == "realbody" | ||||
|  | ||||
|     def testAddFallBackHelper(self): | ||||
|         msg = Message() | ||||
|         msg["body"] = "Great" | ||||
|         msg["feature_fallback"].add_quoted_fallback("Anna wrote:\nHi, how are you?") | ||||
|         # ugly dedent but the test does not pass without it | ||||
|         msg["reply"].add_quoted_fallback("Anna wrote:\nHi, how are you?") | ||||
|         self.check( | ||||
|             msg, | ||||
|             msg,  # language=XML | ||||
|             """ | ||||
|         <message xmlns="jabber:client" type="normal"> | ||||
|             <body>> Anna wrote:\n> Hi, how are you?\nGreat</body> | ||||
|             <reply xmlns="urn:xmpp:reply:0" /> | ||||
|             <fallback xmlns="urn:xmpp:fallback:0" for="urn:xmpp:reply:0"> | ||||
|                 <body start='0' end='33' /> | ||||
|             </fallback> | ||||
| @@ -67,8 +69,8 @@ class TestReply(SlixTest): | ||||
|  | ||||
|         msg = Message() | ||||
|         msg["body"] = "Great" | ||||
|         msg["feature_fallback"].add_quoted_fallback(body) | ||||
|         body2 = msg["feature_fallback"].get_fallback_body() | ||||
|         msg["reply"].add_quoted_fallback(body) | ||||
|         body2 = msg["reply"].get_fallback_body() | ||||
|         self.assertTrue(body2 == quoted, body2) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										76
									
								
								tests/test_stream_xep_0115.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								tests/test_stream_xep_0115.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import logging | ||||
| import unittest | ||||
| from slixmpp.test import SlixTest | ||||
|  | ||||
|  | ||||
| class TestCaps(SlixTest): | ||||
|     def setUp(self): | ||||
|         self.stream_start(plugins=["xep_0115"]) | ||||
|  | ||||
|     def testConcurrentSameHash(self): | ||||
|         """ | ||||
|         Check that we only resolve a given ver string to a disco info once, | ||||
|         even if we receive several presences with that same ver string | ||||
|         consecutively. | ||||
|         """ | ||||
|         self.recv(  # language=XML | ||||
|             """ | ||||
|             <presence from='romeo@montague.lit/orchard'> | ||||
|               <c xmlns='http://jabber.org/protocol/caps' | ||||
|                  hash='sha-1' | ||||
|                  node='a-node' | ||||
|                  ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/> | ||||
|             </presence> | ||||
|             """ | ||||
|         ) | ||||
|         self.recv(  # language=XML | ||||
|             """ | ||||
|             <presence from='i-dont-know-much-shakespeare@montague.lit/orchard'> | ||||
|               <c xmlns='http://jabber.org/protocol/caps' | ||||
|                  hash='sha-1' | ||||
|                  node='a-node' | ||||
|                  ver='h0TdMvqNR8FHUfFG1HauOLYZDqE='/> | ||||
|             </presence> | ||||
|             """ | ||||
|         ) | ||||
|         self.send(  # language=XML | ||||
|             """ | ||||
|             <iq xmlns="jabber:client" | ||||
|                 id="1" | ||||
|                 to="romeo@montague.lit/orchard" | ||||
|                 type="get"> | ||||
|               <query xmlns="http://jabber.org/protocol/disco#info" | ||||
|                      node="a-node#h0TdMvqNR8FHUfFG1HauOLYZDqE="/> | ||||
|             </iq> | ||||
|             """ | ||||
|         ) | ||||
|         self.send(None) | ||||
|         self.recv(  # language=XML | ||||
|             """ | ||||
|             <iq from='romeo@montague.lit/orchard' | ||||
|                 id='1' | ||||
|                 type='result'> | ||||
|               <query xmlns='http://jabber.org/protocol/disco#info' | ||||
|                      node='a-nodes#h0TdMvqNR8FHUfFG1HauOLYZDqE='> | ||||
|                 <identity category='client' name='a client' type='pc'/> | ||||
|                 <feature var='http://jabber.org/protocol/caps'/> | ||||
|               </query> | ||||
|             </iq> | ||||
|             """ | ||||
|         ) | ||||
|         self.send(None) | ||||
|         self.assertTrue( | ||||
|             self.xmpp["xep_0030"].supports( | ||||
|                 "romeo@montague.lit/orchard", "http://jabber.org/protocol/caps" | ||||
|             ) | ||||
|         ) | ||||
|         self.assertTrue( | ||||
|             self.xmpp["xep_0030"].supports( | ||||
|                 "i-dont-know-much-shakespeare@montague.lit/orchard", | ||||
|                 "http://jabber.org/protocol/caps", | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| logging.basicConfig(level=logging.DEBUG) | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestCaps) | ||||
| @@ -1,7 +1,7 @@ | ||||
| import unittest | ||||
|  | ||||
| from slixmpp import ComponentXMPP, Iq, Message | ||||
| from slixmpp.roster import RosterItem | ||||
| from slixmpp import Message, JID, Iq | ||||
| from slixmpp.plugins.xep_0356 import permissions | ||||
| from slixmpp.test import SlixTest | ||||
|  | ||||
|  | ||||
| @@ -9,9 +9,9 @@ class TestPermissions(SlixTest): | ||||
|     def setUp(self): | ||||
|         self.stream_start( | ||||
|             mode="component", | ||||
|             plugins=["xep_0356"], | ||||
|             plugins=["xep_0356", "xep_0045"], | ||||
|             jid="pubsub.capulet.lit", | ||||
|             server="capulet.net", | ||||
|             server="capulet.lit", | ||||
|         ) | ||||
|  | ||||
|     def testPluginEnd(self): | ||||
| @@ -23,26 +23,44 @@ class TestPermissions(SlixTest): | ||||
|         self.assertFalse(exc) | ||||
|  | ||||
|     def testGrantedPrivileges(self): | ||||
|         # https://xmpp.org/extensions/xep-0356.html#example-4 | ||||
|         results = {"event": False} | ||||
|         x = self.xmpp["xep_0356"] | ||||
|         self.xmpp.add_event_handler( | ||||
|             "privileges_advertised", lambda msg: results.__setitem__("event", True) | ||||
|         ) | ||||
|         self.recv( | ||||
|             """ | ||||
|             <message from='capulet.net' to='pubub.capulet.lit' id='54321'> | ||||
|             <message from='capulet.lit' to='pubsub.capulet.lit' id='54321'> | ||||
|                 <privilege xmlns='urn:xmpp:privilege:2'> | ||||
|                     <perm access='roster' type='both'/> | ||||
|                     <perm access='message' type='outgoing'/> | ||||
|                     <perm access='iq'> | ||||
|                       <namespace ns='some_ns' type='get' /> | ||||
|                       <namespace ns='some_other_ns' type='both' /> | ||||
|                     </perm> | ||||
|                 </privilege> | ||||
|             </message> | ||||
|             """ | ||||
|         ) | ||||
|         self.assertEqual(self.xmpp["xep_0356"].granted_privileges["roster"], "both") | ||||
|         server = JID("capulet.lit") | ||||
|         self.assertEqual( | ||||
|             self.xmpp["xep_0356"].granted_privileges["message"], "outgoing" | ||||
|             x.granted_privileges[server].roster, permissions.RosterAccess.BOTH | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             x.granted_privileges[server].message, permissions.MessagePermission.OUTGOING | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             x.granted_privileges[server].presence, permissions.PresencePermission.NONE | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             x.granted_privileges[server].iq["nope"], permissions.IqPermission.NONE | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             x.granted_privileges[server].iq["some_ns"], permissions.IqPermission.GET | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             x.granted_privileges[server].iq["some_other_ns"], permissions.IqPermission.BOTH | ||||
|         ) | ||||
|         self.assertEqual(self.xmpp["xep_0356"].granted_privileges["presence"], "none") | ||||
|         self.assertTrue(results["event"]) | ||||
|  | ||||
|     def testGetRosterIq(self): | ||||
| @@ -94,7 +112,7 @@ class TestPermissions(SlixTest): | ||||
|  | ||||
|     def testMakeOutgoingMessage(self): | ||||
|         xmlstring = """ | ||||
|         <message xmlns="jabber:component:accept" from='pubsub.capulet.lit' to='capulet.net'> | ||||
|         <message xmlns="jabber:component:accept" from='pubsub.capulet.lit' to='capulet.lit'> | ||||
|             <privilege xmlns='urn:xmpp:privilege:2'> | ||||
|                 <forwarded xmlns='urn:xmpp:forward:0'> | ||||
|                     <message from="juliet@capulet.lit" to="romeo@montague.lit" xmlns="jabber:client"> | ||||
| @@ -108,9 +126,49 @@ class TestPermissions(SlixTest): | ||||
|         msg["from"] = "juliet@capulet.lit" | ||||
|         msg["to"] = "romeo@montague.lit" | ||||
|         msg["body"] = "I do not hate you" | ||||
|          | ||||
|  | ||||
|         priv_msg = self.xmpp["xep_0356"]._make_privileged_message(msg) | ||||
|         self.check(priv_msg, xmlstring, use_values=False) | ||||
|  | ||||
|     def testDetectServer(self): | ||||
|         msg = Message() | ||||
|         msg["from"] = "juliet@something" | ||||
|         msg["to"] = "romeo@montague.lit" | ||||
|         msg["body"] = "I do not hate you" | ||||
|  | ||||
|         priv_msg = self.xmpp["xep_0356"]._make_privileged_message(msg) | ||||
|         assert priv_msg.get_to() == "something" | ||||
|         assert priv_msg.get_from() == "pubsub.capulet.lit" | ||||
|  | ||||
|     def testIqOnBehalf(self): | ||||
|         iq = Iq() | ||||
|         iq["mucadmin_query"]["item"]["affiliation"] = "member" | ||||
|         iq.set_from("juliet@xxx") | ||||
|         iq.set_to("somemuc@conf") | ||||
|         iq.set_type("get") | ||||
|         self.xmpp["xep_0356"].granted_privileges["conf"].iq["http://jabber.org/protocol/muc#admin"] = permissions.IqPermission.BOTH | ||||
|         r = self.xmpp.loop.create_task(self.xmpp["xep_0356"].send_privileged_iq(iq, iq_id="0")) | ||||
|         self.send( | ||||
|             """ | ||||
|             <iq from="pubsub.capulet.lit" | ||||
|                 to="juliet@xxx" | ||||
|                 xmlns="jabber:component:accept" | ||||
|                 type="get" id="0"> | ||||
|                 <privileged_iq xmlns='urn:xmpp:privilege:2'> | ||||
|                     <iq xmlns='jabber:client' | ||||
|                         type='get' | ||||
|                         to='somemuc@conf' | ||||
|                         from='juliet@xxx' | ||||
|                          id="0"> | ||||
|                           <query xmlns='http://jabber.org/protocol/muc#admin'> | ||||
|                             <item affiliation='member'/> | ||||
|                           </query> | ||||
|                     </iq> | ||||
|                 </privileged_iq> | ||||
|             </iq> | ||||
|             """, | ||||
|             use_values=False | ||||
|         ) | ||||
|  | ||||
|  | ||||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestPermissions) | ||||
|   | ||||
| @@ -9,8 +9,8 @@ class TestReply(SlixTest): | ||||
|  | ||||
|     def testFallBackBody(self): | ||||
|         async def on_reply(msg): | ||||
|             start = msg["feature_fallback"]["fallback_body"]["start"] | ||||
|             end = msg["feature_fallback"]["fallback_body"]["end"] | ||||
|             start = msg["fallback"]["body"]["start"] | ||||
|             end = msg["fallback"]["body"]["end"] | ||||
|             self.xmpp["xep_0461"].send_reply( | ||||
|                 reply_to=msg.get_from(), | ||||
|                 reply_id=msg.get_id(), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user