Compare commits

..

3 Commits

Author SHA1 Message Date
Emmanuel Gil Peyrot
292f3206f6 Skip tests with known missing idna validation 2024-04-19 14:37:19 +02:00
Emmanuel Gil Peyrot
d1f2e196db Initial Rust version. 2024-04-19 14:30:50 +02:00
Emmanuel Gil Peyrot
f084ad2724 Remove UnescapedJID
It hadn’t been functional for many years, producing invalid JIDs and
being confusing for users anyway.  Better remove it.
2024-04-19 13:57:29 +02:00
63 changed files with 581 additions and 1936 deletions

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

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

7
.gitignore vendored
View File

@@ -14,4 +14,9 @@ slixmpp.egg-info/
.DS_STORE
.idea/
.vscode/
venv/
venv/
# Added by cargo
/target
/Cargo.lock

81
.gitlab-ci.yml Normal file
View File

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

7
.travis.yml Normal file
View File

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

View File

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

View File

@@ -1,20 +1,7 @@
when:
event: [ push, pull_request ]
steps:
test_integration:
image: "python:3.11"
environment:
CI_ACCOUNT1:
from_secret: ci_account1
CI_ACCOUNT1_PASSWORD:
from_secret: ci_account1_password
CI_ACCOUNT2:
from_secret: ci_account2
CI_ACCOUNT2_PASSWORD:
from_secret: ci_account2_password
CI_MUC_SERVER:
from_secret: ci_muc_server
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

View File

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

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "slixmpp"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
jid = "0.10"
pyo3 = "0.21"
[lib]
crate-type = ["cdylib"]

View File

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

View File

@@ -94,4 +94,3 @@ Plugin index
xep_0439
xep_0441
xep_0444
xep_0492

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
Projects Using Slixmpp
======================
This page enumerates software in the form of applications, bots and gateways utilizing the XMPP protocols with slixmpp.
Applications
------------
@@ -10,8 +8,7 @@ sendxmpp-py
~~~~~~~~~~~
sendxmpp is a command line program and is the XMPP equivalent of sendmail. It is a Python version of the original sendxmpp which is written in Perl.
- `Source <https://code.moparisthebest.com/moparisthebest/sendxmpp-py>`_
- `Groupchat <xmpp:xmpp-ircd@chatrooms.hackerposse.com?join>`_
- `Source <https://github.com/moparisthebest/sendxmpp-py>`_
Bots
----
@@ -22,12 +19,6 @@ XMPP bot which logs groupchat messages. Logs are in text format, with one file p
- `Source <https://git.khaganat.net/khaganat/BotLogMauve>`_
BukuBot
~~~~~~~
BukuBot makes it possible to manage and search your bookmarks from your chat.
- `Source <https://codeberg.org/sch/BukuBot>`_
LinkBot
~~~~~~~
This bot reveals the title of any shared link in a groupchat for quick content insight.

View File

@@ -1,184 +0,0 @@
"""
Recognize image file formats based on their first few bytes.
Taken from cpython 3.11 source code before the removal in 3.13.
Licensed under Zero-Clause BSD
"""
from os import PathLike
import warnings
__all__ = ["what"]
warnings._deprecated(__name__, remove=(3, 13))
#-------------------------#
# Recognize image headers #
#-------------------------#
def what(file, h=None):
f = None
try:
if h is None:
if isinstance(file, (str, PathLike)):
f = open(file, 'rb')
h = f.read(32)
else:
location = file.tell()
h = file.read(32)
file.seek(location)
for tf in tests:
res = tf(h, f)
if res:
return res
finally:
if f: f.close()
return None
#---------------------------------#
# Subroutines per image file type #
#---------------------------------#
tests = []
def test_jpeg(h, f):
"""JPEG data with JFIF or Exif markers; and raw JPEG"""
if h[6:10] in (b'JFIF', b'Exif'):
return 'jpeg'
elif h[:4] == b'\xff\xd8\xff\xdb':
return 'jpeg'
tests.append(test_jpeg)
def test_png(h, f):
if h.startswith(b'\211PNG\r\n\032\n'):
return 'png'
tests.append(test_png)
def test_gif(h, f):
"""GIF ('87 and '89 variants)"""
if h[:6] in (b'GIF87a', b'GIF89a'):
return 'gif'
tests.append(test_gif)
def test_tiff(h, f):
"""TIFF (can be in Motorola or Intel byte order)"""
if h[:2] in (b'MM', b'II'):
return 'tiff'
tests.append(test_tiff)
def test_rgb(h, f):
"""SGI image library"""
if h.startswith(b'\001\332'):
return 'rgb'
tests.append(test_rgb)
def test_pbm(h, f):
"""PBM (portable bitmap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
return 'pbm'
tests.append(test_pbm)
def test_pgm(h, f):
"""PGM (portable graymap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
return 'pgm'
tests.append(test_pgm)
def test_ppm(h, f):
"""PPM (portable pixmap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
return 'ppm'
tests.append(test_ppm)
def test_rast(h, f):
"""Sun raster file"""
if h.startswith(b'\x59\xA6\x6A\x95'):
return 'rast'
tests.append(test_rast)
def test_xbm(h, f):
"""X bitmap (X10 or X11)"""
if h.startswith(b'#define '):
return 'xbm'
tests.append(test_xbm)
def test_bmp(h, f):
if h.startswith(b'BM'):
return 'bmp'
tests.append(test_bmp)
def test_webp(h, f):
if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
return 'webp'
tests.append(test_webp)
def test_exr(h, f):
if h.startswith(b'\x76\x2f\x31\x01'):
return 'exr'
tests.append(test_exr)
#--------------------#
# Small test program #
#--------------------#
def test():
import sys
recursive = 0
if sys.argv[1:] and sys.argv[1] == '-r':
del sys.argv[1:2]
recursive = 1
try:
if sys.argv[1:]:
testall(sys.argv[1:], recursive, 1)
else:
testall(['.'], recursive, 1)
except KeyboardInterrupt:
sys.stderr.write('\n[Interrupted]\n')
sys.exit(1)
def testall(list, recursive, toplevel):
import sys
import os
for filename in list:
if os.path.isdir(filename):
print(filename + '/:', end=' ')
if recursive or toplevel:
print('recursing down:')
import glob
names = glob.glob(os.path.join(glob.escape(filename), '*'))
testall(names, recursive, 0)
else:
print('*** directory (use -r) ***')
else:
print(filename + ':', end=' ')
sys.stdout.flush()
try:
print(what(filename))
except OSError:
print('*** not found ***')
if __name__ == '__main__':
test()

View File

@@ -1,184 +0,0 @@
"""
Recognize image file formats based on their first few bytes.
Taken from cpython 3.11 source code before the removal in 3.13.
Licensed under Zero-Clause BSD
"""
from os import PathLike
import warnings
__all__ = ["what"]
warnings._deprecated(__name__, remove=(3, 13))
#-------------------------#
# Recognize image headers #
#-------------------------#
def what(file, h=None):
f = None
try:
if h is None:
if isinstance(file, (str, PathLike)):
f = open(file, 'rb')
h = f.read(32)
else:
location = file.tell()
h = file.read(32)
file.seek(location)
for tf in tests:
res = tf(h, f)
if res:
return res
finally:
if f: f.close()
return None
#---------------------------------#
# Subroutines per image file type #
#---------------------------------#
tests = []
def test_jpeg(h, f):
"""JPEG data with JFIF or Exif markers; and raw JPEG"""
if h[6:10] in (b'JFIF', b'Exif'):
return 'jpeg'
elif h[:4] == b'\xff\xd8\xff\xdb':
return 'jpeg'
tests.append(test_jpeg)
def test_png(h, f):
if h.startswith(b'\211PNG\r\n\032\n'):
return 'png'
tests.append(test_png)
def test_gif(h, f):
"""GIF ('87 and '89 variants)"""
if h[:6] in (b'GIF87a', b'GIF89a'):
return 'gif'
tests.append(test_gif)
def test_tiff(h, f):
"""TIFF (can be in Motorola or Intel byte order)"""
if h[:2] in (b'MM', b'II'):
return 'tiff'
tests.append(test_tiff)
def test_rgb(h, f):
"""SGI image library"""
if h.startswith(b'\001\332'):
return 'rgb'
tests.append(test_rgb)
def test_pbm(h, f):
"""PBM (portable bitmap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r':
return 'pbm'
tests.append(test_pbm)
def test_pgm(h, f):
"""PGM (portable graymap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r':
return 'pgm'
tests.append(test_pgm)
def test_ppm(h, f):
"""PPM (portable pixmap)"""
if len(h) >= 3 and \
h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r':
return 'ppm'
tests.append(test_ppm)
def test_rast(h, f):
"""Sun raster file"""
if h.startswith(b'\x59\xA6\x6A\x95'):
return 'rast'
tests.append(test_rast)
def test_xbm(h, f):
"""X bitmap (X10 or X11)"""
if h.startswith(b'#define '):
return 'xbm'
tests.append(test_xbm)
def test_bmp(h, f):
if h.startswith(b'BM'):
return 'bmp'
tests.append(test_bmp)
def test_webp(h, f):
if h.startswith(b'RIFF') and h[8:12] == b'WEBP':
return 'webp'
tests.append(test_webp)
def test_exr(h, f):
if h.startswith(b'\x76\x2f\x31\x01'):
return 'exr'
tests.append(test_exr)
#--------------------#
# Small test program #
#--------------------#
def test():
import sys
recursive = 0
if sys.argv[1:] and sys.argv[1] == '-r':
del sys.argv[1:2]
recursive = 1
try:
if sys.argv[1:]:
testall(sys.argv[1:], recursive, 1)
else:
testall(['.'], recursive, 1)
except KeyboardInterrupt:
sys.stderr.write('\n[Interrupted]\n')
sys.exit(1)
def testall(list, recursive, toplevel):
import sys
import os
for filename in list:
if os.path.isdir(filename):
print(filename + '/:', end=' ')
if recursive or toplevel:
print('recursing down:')
import glob
names = glob.glob(os.path.join(glob.escape(filename), '*'))
testall(names, recursive, 0)
else:
print('*** directory (use -r) ***')
else:
print(filename + ':', end=' ')
sys.stdout.flush()
try:
print(what(filename))
except OSError:
print('*** not found ***')
if __name__ == '__main__':
test()

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,9 +27,3 @@ from slixmpp.clientxmpp import ClientXMPP
from slixmpp.componentxmpp import ComponentXMPP
from slixmpp.version import __version__, __version_info__
__all__ = [
'Message', 'Presence', 'Iq', 'JID', 'InvalidJID', 'ET', 'ElementBase',
'register_stanza_plugin', 'XMLStream', 'BaseXMPP', 'ClientXMPP', 'ComponentXMPP',
'__version__', '__version_info__'
]

View File

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

View File

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

View File

@@ -1,445 +1 @@
# slixmpp.jid
# ~~~~~~~~~~~~~~~~~~~~~~~
# This module allows for working with Jabber IDs (JIDs).
# Part of Slixmpp: The Slick XMPP Library
# :copyright: (c) 2011 Nathanael C. Fritz
# :license: MIT, see LICENSE for more details
from __future__ import annotations
import re
import socket
from functools import lru_cache
from typing import (
Optional,
Union,
)
from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError
HAVE_INET_PTON = hasattr(socket, 'inet_pton')
#: The basic regex pattern that a JID must match in order to determine
#: the local, domain, and resource parts. This regex does NOT do any
#: validation, which requires application of nodeprep, resourceprep, etc.
JID_PATTERN = re.compile(
"^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
)
#: The set of escape sequences for the characters not allowed by nodeprep.
JID_ESCAPE_SEQUENCES = {'\\20', '\\22', '\\26', '\\27', '\\2f',
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'}
#: The reverse mapping of escape sequences to their original forms.
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
'\\22': '"',
'\\26': '&',
'\\27': "'",
'\\2f': '/',
'\\3a': ':',
'\\3c': '<',
'\\3e': '>',
'\\40': '@',
'\\5c': '\\'}
# TODO: Find the best cache size for a standard usage.
@lru_cache(maxsize=1024)
def _parse_jid(data: str):
"""
Parse string data into the node, domain, and resource
components of a JID, if possible.
:param string data: A string that is potentially a JID.
:raises InvalidJID:
:returns: tuple of the validated local, domain, and resource strings
"""
match = JID_PATTERN.match(data)
if not match:
raise InvalidJID('JID could not be parsed')
(node, domain, resource) = match.groups()
node = _validate_node(node)
domain = _validate_domain(domain)
resource = _validate_resource(resource)
return node, domain, resource
def _validate_node(node: Optional[str]):
"""Validate the local, or username, portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by nodeprep.
"""
if node is None:
return ''
try:
node = nodeprep(node)
except StringprepError:
raise InvalidJID('Nodeprep failed')
if not node:
raise InvalidJID('Localpart must not be 0 bytes')
if len(node) > 1023:
raise InvalidJID('Localpart must be less than 1024 bytes')
return node
def _validate_domain(domain: str):
"""Validate the domain portion of a JID.
IP literal addresses are left as-is, if valid. Domain names
are stripped of any trailing label separators (`.`), and are
checked with the nameprep profile of stringprep. If the given
domain is actually a punyencoded version of a domain name, it
is converted back into its original Unicode form. Domains must
also not start or end with a dash (`-`).
:raises InvalidJID:
:returns: The validated domain name
"""
ip_addr = False
# First, check if this is an IPv4 address
try:
socket.inet_aton(domain)
ip_addr = True
except socket.error:
pass
# Check if this is an IPv6 address
if not ip_addr and HAVE_INET_PTON and domain[0] == '[' and domain[-1] == ']':
try:
ip = domain[1:-1]
socket.inet_pton(socket.AF_INET6, ip)
ip_addr = True
except (socket.error, ValueError):
pass
if not ip_addr:
# This is a domain name, which must be checked further
if domain and domain[-1] == '.':
domain = domain[:-1]
try:
domain = idna(domain)
except StringprepError:
raise InvalidJID(f'idna validation failed: {domain}')
if ':' in domain:
raise InvalidJID(f'Domain containing a port: {domain}')
for label in domain.split('.'):
if not label:
raise InvalidJID(f'Domain containing too many dots: {domain}')
if '-' in (label[0], label[-1]):
raise InvalidJID(f'Domain starting or ending with -: {domain}')
if not domain:
raise InvalidJID('Domain must not be 0 bytes')
if len(domain) > 1023:
raise InvalidJID('Domain must be less than 1024 bytes')
return domain
def _validate_resource(resource: Optional[str]):
"""Validate the resource portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by resourceprep.
"""
if resource is None:
return ''
try:
resource = resourceprep(resource)
except StringprepError:
raise InvalidJID('Resourceprep failed')
if not resource:
raise InvalidJID('Resource must not be 0 bytes')
if len(resource) > 1023:
raise InvalidJID('Resource must be less than 1024 bytes')
return resource
def _unescape_node(node: str):
"""Unescape a local portion of a JID.
.. note::
The unescaped local portion is meant ONLY for presentation,
and should not be used for other purposes.
"""
unescaped = []
seq = ''
for i, char in enumerate(node):
if char == '\\':
seq = node[i:i+3]
if seq not in JID_ESCAPE_SEQUENCES:
seq = ''
if seq:
if len(seq) == 3:
unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
# Pop character off the escape sequence, and ignore it
seq = seq[1:]
else:
unescaped.append(char)
return ''.join(unescaped)
def _format_jid(
local: Optional[str] = None,
domain: Optional[str] = None,
resource: Optional[str] = None,
):
"""Format the given JID components into a full or bare JID.
:param string local: Optional. The local portion of the JID.
:param string domain: Required. The domain name portion of the JID.
:param strin resource: Optional. The resource portion of the JID.
:return: A full or bare JID string.
"""
if domain is None:
return ''
if local is not None:
result = local + '@' + domain
else:
result = domain
if resource is not None:
result += '/' + resource
return result
class InvalidJID(ValueError):
"""
Raised when attempting to create a JID that does not pass validation.
It can also be raised if modifying an existing JID in such a way as
to make it invalid, such trying to remove the domain from an existing
full JID while the local and resource portions still exist.
"""
# pylint: disable=R0903
class UnescapedJID:
"""
.. versionadded:: 1.1.10
"""
__slots__ = ('_node', '_domain', '_resource')
def __init__(
self,
node: Optional[str],
domain: Optional[str],
resource: Optional[str],
):
self._node = node
self._domain = domain
self._resource = resource
def __getattribute__(self, name: str):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
return self._resource or ''
if name in ('user', 'username', 'local', 'node'):
return self._node or ''
if name in ('server', 'domain', 'host'):
return self._domain or ''
if name in ('full', 'jid'):
return _format_jid(self._node, self._domain, self._resource)
if name == 'bare':
return _format_jid(self._node, self._domain)
return object.__getattribute__(self, name)
def __str__(self):
"""Use the full JID as the string value."""
return _format_jid(self._node, self._domain, self._resource)
def __repr__(self):
"""Use the full JID as the representation."""
return _format_jid(self._node, self._domain, self._resource)
class JID:
"""
A representation of a Jabber ID, or JID.
Each JID may have three components: a user, a domain, and an optional
resource. For example: user@domain/resource
When a resource is not used, the JID is called a bare JID.
The JID is a full JID otherwise.
**JID Properties:**
:full: The string value of the full JID.
:jid: Alias for ``full``.
:bare: The string value of the bare JID.
:node: The node portion of the JID.
:user: Alias for ``node``.
:local: Alias for ``node``.
:username: Alias for ``node``.
:domain: The domain name portion of the JID.
:server: Alias for ``domain``.
:host: Alias for ``domain``.
:resource: The resource portion of the 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, bare: bool = False):
if not jid:
self._node = ''
self._domain = ''
self._resource = ''
self._bare = ''
self._full = ''
return
elif not isinstance(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 if not bare else ''
self._update_bare_full()
def unescape(self):
"""Return an unescaped JID object.
Using an unescaped JID is preferred for displaying JIDs
to humans, and they should NOT be used for any other
purposes than for presentation.
:return: :class:`UnescapedJID`
.. versionadded:: 1.1.10
"""
return UnescapedJID(_unescape_node(self._node),
self._domain,
self._resource)
def _update_bare_full(self):
"""Format the given JID into a bare and a full JID.
"""
self._bare = (self._node + '@' + self._domain
if self._node
else self._domain)
self._full = (self._bare + '/' + self._resource
if self._resource
else self._bare)
@property
def bare(self) -> str:
return self._bare
@bare.setter
def bare(self, value: str):
node, domain, resource = _parse_jid(value)
assert not resource
self._node = node
self._domain = domain
self._update_bare_full()
@property
def node(self) -> str:
return self._node
@node.setter
def node(self, value: Optional[str]):
self._node = _validate_node(value)
self._update_bare_full()
@property
def domain(self) -> str:
return self._domain
@domain.setter
def domain(self, value: str):
self._domain = _validate_domain(value)
self._update_bare_full()
@property
def resource(self) -> str:
return self._resource
@resource.setter
def resource(self, value: Optional[str]):
self._resource = _validate_resource(value)
self._update_bare_full()
@property
def full(self) -> str:
return self._full
@full.setter
def full(self, value: str):
self._node, self._domain, self._resource = _parse_jid(value)
self._update_bare_full()
user = node
local = node
username = node
server = domain
host = domain
jid = full
def __str__(self):
"""Use the full JID as the string value."""
return self._full
def __repr__(self):
"""Use the full JID as the representation."""
return self._full
# pylint: disable=W0212
def __eq__(self, other):
"""Two JIDs are equal if they have the same full JID value."""
if isinstance(other, UnescapedJID):
return False
if not isinstance(other, JID):
try:
other = JID(other)
except InvalidJID:
return NotImplemented
return (self._node == other._node and
self._domain == other._domain and
self._resource == other._resource)
def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal."""
return not self == other
def __hash__(self):
"""Hash a JID based on the string version of its full JID."""
return hash(self._full)
from libslixmpp import JID, InvalidJID

View File

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

View File

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

View File

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

View File

@@ -165,11 +165,11 @@ class DiscoInfo(ElementBase):
identities = []
for id_xml in self.xml.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
category = id_xml.attrib.get('category', None)
type_ = id_xml.attrib.get('type', None)
name = id_xml.attrib.get('name', None)
if lang is None or xml_lang == lang:
id = (category, type_, xml_lang, name)
id = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None))
if isinstance(identities, set):
identities.add(id)
else:
@@ -253,12 +253,10 @@ class DiscoInfo(ElementBase):
else:
features = []
for feature_xml in self.xml.findall('{%s}feature' % self.namespace):
feature = feature_xml.attrib.get('var', None)
if feature:
if isinstance(features, set):
features.add(feature)
else:
features.append(feature)
if isinstance(features, set):
features.add(feature_xml.attrib['var'])
else:
features.append(feature_xml.attrib['var'])
return features
def set_features(self, features: Iterable[str]):

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
# See the file LICENSE for copying permissio
import logging
import slixmpp
from slixmpp.stanza import Message
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
@@ -44,8 +45,5 @@ class XEP_0308(BasePlugin):
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
def is_correction(self, msg: Message):
return msg.xml.find('{%s}replace' % Replace.namespace) is not None
def _handle_correction(self, msg: Message):
def _handle_correction(self, msg):
self.xmpp.event('message_correction', msg)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,11 +30,11 @@ class Reply(ElementBase):
if nickname:
quoted = "> " + nickname + ":\n" + quoted
msg["body"] = quoted + msg["body"]
fallback_elem = Fallback()
fallback_elem["for"] = NS
fallback_elem["body"]["start"] = 0
fallback_elem["body"]["end"] = len(quoted)
msg.append(fallback_elem)
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()
@@ -50,23 +50,6 @@ class Reply(ElementBase):
return body[start:end]
else:
return ""
def strip_fallback_content(self) -> str:
msg = self.parent()
for fallback in msg["fallbacks"]:
if fallback["for"] == NS:
break
else:
return msg["body"]
start = fallback["body"]["start"]
end = fallback["body"]["end"]
body = msg["body"]
if 0 <= start < end <= len(body):
return body[:start] + body[end:]
else:
return body
def register_plugins():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -281,8 +281,7 @@ class XMLStream(asyncio.BaseProtocol):
__slow_tasks: List[Task]
__queued_stanzas: List[Tuple[Union[StanzaBase, str], bool]]
def __init__(self, host: str = '', port: int = 0,
ssl_context: Optional[ssl.SSLContext] = None):
def __init__(self, host: str = '', port: int = 0):
self.transport = None
self.socket = None
self._connect_loop_wait = 0
@@ -299,12 +298,9 @@ class XMLStream(asyncio.BaseProtocol):
# A dict of {name: handle}
self.scheduled_events = {}
if ssl_context is None:
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = True
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
else:
self.ssl_context = ssl_context
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = True
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.event_when_connected = "connected"
@@ -1354,7 +1350,6 @@ class XMLStream(asyncio.BaseProtocol):
if isinstance(data, (RootStanza, str)) and not passthrough:
self.__queued_stanzas.append((data, use_filters))
log.debug('NOT SENT: %s %s', type(data), data)
self.event('stanza_not_sent', data)
return
self.waiting_queue.put_nowait((data, use_filters))
@@ -1418,11 +1413,7 @@ class XMLStream(asyncio.BaseProtocol):
# Convert the raw XML object into a stanza object. If no registered
# stanza type applies, a generic StanzaBase stanza will be used.
try:
stanza: Optional[StanzaBase] = self._build_stanza(xml)
except Exception as exc:
log.exception("Unable to parse stanza: %s,\n%s", exc, xml)
stanza = None
stanza: Optional[StanzaBase] = self._build_stanza(xml)
for filter in self.__filters['in']:
if stanza is not None:
filter = cast(SyncFilter, filter)

278
src/lib.rs Normal file
View File

@@ -0,0 +1,278 @@
use pyo3::exceptions::{PyNotImplementedError, PyValueError};
use pyo3::prelude::*;
pyo3::create_exception!(py_jid, InvalidJID, PyValueError, "Raised when attempting to create a JID that does not pass validation.\n\nIt can also be raised if modifying an existing JID in such a way as\nto make it invalid, such trying to remove the domain from an existing\nfull JID while the local and resource portions still exist.");
fn to_exc(err: jid::Error) -> PyErr {
InvalidJID::new_err(err.to_string())
}
/// A representation of a Jabber ID, or JID.
///
/// Each JID may have three components: a user, a domain, and an optional resource. For example:
/// user@domain/resource
///
/// When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise.
///
/// Raises InvalidJID if the parser rejects it.
#[pyclass(name = "JID", module = "slixmpp.jid")]
struct PyJid {
jid: Option<jid::Jid>,
}
#[pymethods]
impl PyJid {
#[new]
#[pyo3(signature = (jid=None, bare=false))]
fn new(jid: Option<&Bound<'_, PyAny>>, bare: bool) -> PyResult<Self> {
if let Some(jid) = jid {
if let Ok(py_jid) = jid.extract::<PyRef<PyJid>>() {
if bare {
if let Some(py_jid) = &(*py_jid).jid {
Ok(PyJid {
jid: Some(jid::Jid::Bare(py_jid.to_bare())),
})
} else {
Ok(PyJid { jid: None })
}
} else {
Ok(PyJid {
jid: (*py_jid).jid.clone(),
})
}
} else {
let jid: &str = jid.extract()?;
if jid.is_empty() {
Ok(PyJid { jid: None })
} else {
let mut jid = jid::Jid::new(jid).map_err(to_exc)?;
if bare {
jid = jid::Jid::Bare(jid.into_bare())
}
Ok(PyJid { jid: Some(jid) })
}
}
} else {
Ok(PyJid { jid: None })
}
}
/*
// TODO: implement or remove from the API
fn unescape() {
}
*/
#[getter]
fn get_bare(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_bare().to_string(),
}
}
#[setter]
fn set_bare(&mut self, bare: &str) -> PyResult<()> {
let bare = jid::BareJid::new(bare).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(_)) | None => jid::Jid::Bare(bare),
Some(jid::Jid::Full(jid)) => jid::Jid::Full(bare.with_resource(&jid.resource())),
});
Ok(())
}
#[getter]
fn get_full(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
#[setter]
fn set_full(&mut self, full: &str) -> PyResult<()> {
// JID.full = 'domain' is acceptable in slixmpp.
self.jid = Some(jid::Jid::new(full).map_err(to_exc)?);
Ok(())
}
#[getter]
fn get_node(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid
.node_str()
.map(ToString::to_string)
.unwrap_or_else(String::new),
}
}
#[setter]
fn set_node(&mut self, node: &str) -> PyResult<()> {
let node = jid::NodePart::new(node).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => {
jid::Jid::Bare(jid::BareJid::from_parts(Some(&node), &jid.domain()))
}
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
Some(&node),
&jid.domain(),
&jid.resource(),
)),
None => Err(InvalidJID::new_err("JID.node must apply to a proper JID"))?,
});
Ok(())
}
#[getter]
fn get_domain(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.domain_str().to_string(),
}
}
#[setter]
fn set_domain(&mut self, domain: &str) -> PyResult<()> {
let domain = jid::DomainPart::new(domain).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => {
jid::Jid::Bare(jid::BareJid::from_parts(jid.node().as_ref(), &domain))
}
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
jid.node().as_ref(),
&domain,
&jid.resource(),
)),
None => jid::Jid::Bare(jid::BareJid::from_parts(None, &domain)),
});
Ok(())
}
#[getter]
fn get_resource(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid
.resource_str()
.map(ToString::to_string)
.unwrap_or_else(String::new),
}
}
#[setter]
fn set_resource(&mut self, resource: &str) -> PyResult<()> {
let resource = jid::ResourcePart::new(resource).map_err(to_exc)?;
self.jid = Some(match &self.jid {
Some(jid::Jid::Bare(jid)) => jid::Jid::Full(jid.with_resource(&resource)),
Some(jid::Jid::Full(jid)) => jid::Jid::Full(jid::FullJid::from_parts(
jid.node().as_ref(),
&jid.domain(),
&resource,
)),
None => Err(InvalidJID::new_err(
"JID.resource must apply to a proper JID",
))?,
});
Ok(())
}
/// Use the full JID as the string value.
fn __str__(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
/// Use the full JID as the representation.
fn __repr__(&self) -> String {
match &self.jid {
None => String::new(),
Some(jid) => jid.to_string(),
}
}
/// Two JIDs are equal if they have the same full JID value.
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: pyo3::basic::CompareOp) -> PyResult<bool> {
let other = if let Ok(other) = other.extract::<PyRef<PyJid>>() {
other
} else if other.is_none() {
Bound::new(other.py(), PyJid::new(None, false)?)?.borrow()
} else {
Bound::new(other.py(), PyJid::new(Some(other), false)?)?.borrow()
};
match (&self.jid, &other.jid) {
(None, None) => Ok(true),
(Some(jid), Some(other)) => match op {
pyo3::basic::CompareOp::Eq => Ok(jid == other),
pyo3::basic::CompareOp::Ne => Ok(jid != other),
_ => Err(PyNotImplementedError::new_err(
"Only == and != are implemented",
)),
},
_ => Ok(false),
}
}
/// Hash a JID based on the string version of its full JID.
fn __hash__(&self) -> isize {
if let Some(jid) = &self.jid {
// Use the same algorithm as the Python JID.
let string = jid.to_string();
unsafe { pyo3::ffi::_Py_HashBytes(string.as_ptr() as *const _, string.len() as isize) }
} else {
0
}
}
// Aliases
#[getter]
fn get_user(&self) -> String {
self.get_node()
}
#[setter]
fn set_user(&mut self, user: &str) -> PyResult<()> {
self.set_node(user)
}
#[getter]
fn get_server(&self) -> String {
self.get_domain()
}
#[setter]
fn set_server(&mut self, server: &str) -> PyResult<()> {
self.set_domain(server)
}
#[getter]
fn get_host(&self) -> String {
self.get_domain()
}
#[setter]
fn set_host(&mut self, host: &str) -> PyResult<()> {
self.set_domain(host)
}
#[getter]
fn get_jid(&self) -> String {
self.get_full()
}
#[setter]
fn set_jid(&mut self, jid: &str) -> PyResult<()> {
self.set_full(jid)
}
}
#[pymodule]
#[pyo3(name = "libslixmpp")]
fn py_jid(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyJid>()?;
m.add("InvalidJID", py.get_type_bound::<InvalidJID>())?;
Ok(())
}

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import unittest
from slixmpp.test import SlixTest
from slixmpp import JID, InvalidJID
from slixmpp.jid import nodeprep
class TestJIDClass(SlixTest):
@@ -192,10 +191,12 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, 'test.com/%s' % resource)
self.assertRaises(InvalidJID, JID, 'user@test.com/%s' % resource)
@unittest.skip('Rust')
def testTooLongDomainLabel(self):
domain = ('a' * 64) + '.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainEmptyLabel(self):
domain = 'aaa..bbb.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@@ -216,6 +217,7 @@ class TestJIDClass(SlixTest):
jid3 = JID('%s/resource' % domain)
jid4 = JID('user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6NoBrackets(self):
domain = '::1'
@@ -224,6 +226,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6MissingBracket(self):
domain = '[::1'
@@ -232,6 +235,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainInvalidIPv6WrongBracket(self):
domain = '[::]1]'
@@ -240,6 +244,7 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainWithPort(self):
domain = 'example.com:5555'
@@ -248,12 +253,14 @@ class TestJIDClass(SlixTest):
self.assertRaises(InvalidJID, JID, '%s/resource' % domain)
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testDomainWithTrailingDot(self):
domain = 'example.com.'
jid = JID('user@%s/resource' % domain)
self.assertEqual(jid.domain, 'example.com')
@unittest.skip('Rust')
def testDomainWithDashes(self):
domain = 'example.com-'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@@ -261,21 +268,13 @@ class TestJIDClass(SlixTest):
domain = '-example.com'
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
@unittest.skip('Rust')
def testACEDomain(self):
domain = 'xn--bcher-kva.ch'
jid = JID('user@%s/resource' % domain)
self.assertEqual(jid.domain.encode('utf-8'), b'b\xc3\xbccher.ch')
def testJIDUnescape(self):
jid = JID('here\\27s_a_wild_\\26_\\2fcr%zy\\2f_\\40ddress\\20for\\3a\\3cwv\\3e(\\22IMPS\\22)\\5c@example.com')
ujid = jid.unescape()
self.assertEqual(ujid.local, 'here\'s_a_wild_&_/cr%zy/_@ddress for:<wv>("imps")\\')
jid = JID('blah\\5cfoo\\5c20bar@example.com')
ujid = jid.unescape()
self.assertEqual(ujid.local, 'blah\\foo\\20bar')
def testStartOrEndWithEscapedSpaces(self):
local = ' foo'
self.assertRaises(InvalidJID, JID, '%s@example.com' % local)
@@ -288,9 +287,5 @@ class TestJIDClass(SlixTest):
#self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2')
#self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20')
def testNodePrepIdemptotent(self):
node = 'ᴹᴵᴷᴬᴱᴸ'
self.assertEqual(nodeprep(node), nodeprep(nodeprep(node)))
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
tox.ini Normal file
View File

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