Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-nbxmpp for openSUSE:Factory 
checked in at 2026-10-29 16:11:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-nbxmpp (Old)
 and      /work/SRC/openSUSE:Factory/.python-nbxmpp.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-nbxmpp"

Thu Oct 29 16:11:22 2026 rev:59 rq:1393621 version:7.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-nbxmpp/python-nbxmpp.changes      
2026-04-21 12:46:25.674424351 +0200
+++ /work/SRC/openSUSE:Factory/.python-nbxmpp.new.1255/python-nbxmpp.changes    
2026-10-29 16:13:01.892898228 +0100
@@ -1,0 +2,22 @@
+Tue Oct 27 21:02:53 UTC 2026 - Bjørn Lie <[email protected]>
+
+- Update to version 7.4.0:
+  * New: Client: Add better disconnect check
+  * Improvements:
+    + VCardAvatars: Validate and normalize sha1
+    + Fallback: Improve range validation
+    + Use secrets module instead of random
+    + OMEMO: Harden bundle parser
+    + OMEMO: Harden devicelist parser
+  * Bug Fixes:
+    + WebSocket: Disconnect on parsing errors
+    + VCardTemp: Guard against base64 decoding errors
+    + Client: Add timeout when waiting for stream end
+    + MDS: Add request_items dependency
+- Changes from version 7.3.0:
+  * New: Add helper method parse_account_invite()
+  * Improvements: OpenPGP: Don't add fallback body text
+  * Bug Fixes: EME: Make name always a string
+- Update version dependencies according to meson.build.
+
+-------------------------------------------------------------------

Old:
----
  python-nbxmpp-7.2.0.tar.bz2

New:
----
  python-nbxmpp-7.4.0.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-nbxmpp.spec ++++++
--- /var/tmp/diff_new_pack.LUdKpr/_old  2026-10-29 16:13:02.596927753 +0100
+++ /var/tmp/diff_new_pack.LUdKpr/_new  2026-10-29 16:13:02.603928047 +0100
@@ -19,7 +19,7 @@
 %define _name   nbxmpp
 %{?sle15_python_module_pythons}
 Name:           python-nbxmpp
-Version:        7.2.0
+Version:        7.4.0
 Release:        0
 Summary:        XMPP library by Gajim team
 License:        GPL-3.0-or-later
@@ -33,7 +33,7 @@
 BuildRequires:  %{python_module packaging}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module precis-i18n >= 1.0.0}
-BuildRequires:  %{python_module setuptools >= 65.0}
+BuildRequires:  %{python_module setuptools >= 65.0.0}
 BuildRequires:  %{python_module sqlite3}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes

++++++ python-nbxmpp-7.2.0.tar.bz2 -> python-nbxmpp-7.4.0.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/ChangeLog 
new/python-nbxmpp-7.4.0/ChangeLog
--- old/python-nbxmpp-7.2.0/ChangeLog   2026-04-19 19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/ChangeLog   2026-08-26 18:54:51.000000000 +0200
@@ -1,3 +1,38 @@
+nbxmpp 7.4.0 (26 Aug 2026)
+
+  New
+
+  * Client: Add better disconnect check
+
+  Improvements
+
+  * VCardAvatars: Validate and normalize sha1
+  * Fallback: Improve range validation
+  * Use secrets module instead of random
+  * OMEMO: Harden bundle parser
+  * OMEMO: Harden devicelist parser
+
+  Bug Fixes
+
+  * WebSocket: Disconnect on parsing errors
+  * VCardTemp: Guard against base64 decoding errors
+  * Client: Add timeout when waiting for stream end
+  * MDS: Add request_items dependency
+
+nbxmpp 7.3.0 (30 Jun 2026)
+
+  New
+
+  * Add helper method parse_account_invite()
+
+  Improvements
+
+  * OpenPGP: Don't add fallback body text
+
+  Bug Fixes
+
+  * EME: Make name always a string
+
 nbxmpp 7.2.0 (19 Apr 2026)
 
   New
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/__init__.py 
new/python-nbxmpp-7.4.0/nbxmpp/__init__.py
--- old/python-nbxmpp-7.2.0/nbxmpp/__init__.py  2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/__init__.py  2026-08-26 18:54:51.000000000 
+0200
@@ -4,4 +4,4 @@
 
 from .protocol import *  # noqa: F403, E402
 
-__version__: str = "7.2.0"
+__version__: str = "7.4.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/client.py 
new/python-nbxmpp-7.4.0/nbxmpp/client.py
--- old/python-nbxmpp-7.2.0/nbxmpp/client.py    2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/client.py    2026-08-26 18:54:51.000000000 
+0200
@@ -28,9 +28,6 @@
 from nbxmpp.const import StreamError
 from nbxmpp.const import StreamState
 from nbxmpp.dispatcher import StanzaDispatcher
-from nbxmpp.errors import CancelledError
-from nbxmpp.errors import StanzaError
-from nbxmpp.errors import TimeoutStanzaError
 from nbxmpp.namespaces import Namespace
 from nbxmpp.protocol import BindRequest
 from nbxmpp.protocol import Features
@@ -118,6 +115,11 @@
 log = logging.getLogger("nbxmpp.stream")
 
 
+STREAM_END_TIMEOUT_SEC = 2
+CONNECTION_CHECK_INTERVAL = 30
+CONNECTION_CHECK_TIMEOUT = 3
+
+
 class Client(Observable):
     def __init__(self, log_context: str | None = None) -> None:
         """
@@ -164,14 +166,14 @@
 
         self._sm_disabled = False
 
+        self._stream_end_timeout_id: int | None = None
         self._stream_id: str | None = None
         self._stream_secure = False
         self._stream_authenticated = False
         self._stream_features: Features | None = None
         self._session_required = False
         self._connect_successful = False
-        self._stream_close_initiated = False
-        self._ping_task: Task | None = None
+        self._connection_check_timeout_id: int | None = None
         self._error: tuple[StreamError | None, str | None, str | None] = (
             None,
             None,
@@ -191,7 +193,6 @@
 
         self._fallback_ns: set[str] = set()
 
-        self._ping_source_id: int | None = None
         self._tasks: list[Task] = []
 
         self._dispatcher = StanzaDispatcher(self)
@@ -204,6 +205,8 @@
 
         self._state: StreamState = StreamState.DISCONNECTED
 
+        self._connection_check_id = self._enable_connection_check()
+
     def add_task(self, task: Task) -> None:
         self._tasks.append(task)
 
@@ -569,19 +572,48 @@
 
         self._disconnect(immediate=immediate)
 
-    def _disconnect(self, immediate: bool = True) -> None:
-        self.state = StreamState.DISCONNECTING
-        self._remove_ping_timer()
-        self._cancel_ping_task()
+    def _start_stream_end_timeout(self) -> None:
+        if self._stream_end_timeout_id is not None:
+            return
 
-        if not immediate:
-            self._stream_close_initiated = True
-            assert self._smacks is not None
-            self._smacks.close_session()
-            self._end_stream()
-            self._con.shutdown_output()
-        else:
+        self._log.info("Start stream end timeout")
+        self._stream_end_timeout_id = GLib.timeout_add_seconds(
+            STREAM_END_TIMEOUT_SEC, self._on_stream_end_timeout
+        )
+
+    def _remove_stream_end_timeout(self) -> None:
+        if self._stream_end_timeout_id is None:
+            return
+
+        self._log.info("Remove stream end timeout")
+        GLib.source_remove(self._stream_end_timeout_id)
+        self._stream_end_timeout_id = None
+
+    def _on_stream_end_timeout(self) -> None:
+        self._stream_end_timeout_id = None
+        if self._state != StreamState.DISCONNECTING:
+            return
+
+        self._log.warning("Timeout while waiting for stream end")
+        self._con.shutdown_input()
+
+    def _disconnect(self, immediate: bool = True) -> None:
+        self._log.info("Disconnect immediate=%s", immediate)
+        if immediate:
+            self.state = StreamState.DISCONNECTING
+            self._remove_connection_check_timeout()
             self._con.disconnect()
+        else:
+            self._start_stream_end_timeout()
+            self._graceful_disconnect()
+
+    def _graceful_disconnect(self) -> None:
+        self.state = StreamState.DISCONNECTING
+        self._remove_connection_check_timeout()
+        assert self._smacks is not None
+        self._smacks.close_session()
+        self._end_stream()
+        self._con.shutdown_output()
 
     def send(self, stanza: Protocol, *args: Any, **kwargs: Any) -> str:
         # Alias for backwards compat
@@ -596,8 +628,6 @@
         self.state = StreamState.DISCONNECTED
         for task in self._tasks:
             task.cancel()
-        self._remove_ping_timer()
-        self._cancel_ping_task()
         self._reset_stream()
         self.notify("disconnected")
 
@@ -637,15 +667,12 @@
         if not self.has_error:
             self._set_error(StreamError.STREAM, error or "stream-end")
 
+        self._remove_stream_end_timeout()
         self._con.shutdown_input()
-        if not self._stream_close_initiated:
-            self.state = StreamState.DISCONNECTING
-            self._remove_ping_timer()
-            self._cancel_ping_task()
-            assert self._smacks is not None
-            self._smacks.close_session()
-            self._end_stream()
-            self._con.shutdown_output()
+        if self._state not in (StreamState.DISCONNECTING, 
StreamState.DISCONNECTED):
+            # If we are in DISCONNECTING or DISCONNECTED this means we 
initiated
+            # the disconnect, so we dont need to call disconnect again.
+            self._graceful_disconnect()
 
     def _reset_stream(self) -> None:
         self._stream_id = None
@@ -800,33 +827,56 @@
     def _on_data_received(
         self, _connection: Connection, _signal_name: str, data: str
     ) -> None:
+        self._remove_connection_check_timeout()
         self._dispatcher.process_data(data)
-        self._reset_ping_timer()
-
-    def _reset_ping_timer(self) -> None:
-        if self.is_websocket:
-            return
 
-        if not self._mode.is_client:
+    def _enable_connection_check(self) -> int:
+        def _on_check() -> bool:
+            if self._state == StreamState.ACTIVE:
+                self.check_if_connected()
+            return GLib.SOURCE_CONTINUE
+
+        return GLib.timeout_add_seconds(CONNECTION_CHECK_INTERVAL, _on_check)
+
+    def check_if_connected(self) -> None:
+        if self._connection_check_timeout_id is not None:
+            self._log.info(
+                "Ignore connection check request, because request is already 
in progress"
+            )
             return
 
-        if self.state != StreamState.ACTIVE:
+        if self._state != StreamState.ACTIVE:
+            self._log.info(
+                "Ignore connection check request, because stream state is %s",
+                self._state,
+            )
             return
 
-        if self._ping_source_id is not None:
-            self._log.info("Remove ping timer")
-            GLib.source_remove(self._ping_source_id)
-            self._ping_source_id = None
+        self._log.info("Start connection check")
+        self._connection_check_timeout_id = GLib.timeout_add_seconds(
+            CONNECTION_CHECK_TIMEOUT, self._on_connection_check_timeout
+        )
+        assert self._smacks is not None
+        if self._smacks.sm_supported:
+            self._smacks.request_ack()
+        else:
+            self.get_module("Ping").ping(self.domain, timeout=5)
 
-        self._log.info("Start ping timer")
-        self._ping_source_id = GLib.timeout_add_seconds(180, self._ping)
+    def _on_connection_check_timeout(self) -> None:
+        self._connection_check_timeout_id = None
+        if self._state == StreamState.ACTIVE:
+            self._log.info("Connection check timeout")
+            self.disconnect(immediate=True)
 
-    def _remove_ping_timer(self) -> None:
-        if self._ping_source_id is None:
+    def _remove_connection_check_timeout(self) -> None:
+        if self._connection_check_timeout_id is None:
             return
-        self._log.info("Remove ping timer")
-        GLib.source_remove(self._ping_source_id)
-        self._ping_source_id = None
+
+        GLib.source_remove(self._connection_check_timeout_id)
+        self._connection_check_timeout_id = None
+
+        if self._state == StreamState.ACTIVE:
+            self._log.info("Connection check successful")
 
     def send_stanza(
         self,
@@ -1103,34 +1153,6 @@
                 StreamError.SESSION, stanza.getError(), stanza.getErrorMsg()
             )
 
-    def _ping(self) -> None:
-        self._ping_source_id = None
-        self._ping_task = self.get_module("Ping").ping(
-            self.domain, timeout=10, callback=self._on_pong
-        )
-
-    def _on_pong(self, task: Task) -> None:
-        self._ping_task = None
-
-        try:
-            task.finish()
-        except TimeoutStanzaError:
-            self._log.info("Ping timeout")
-            self._disconnect(immediate=True)
-            return
-
-        except CancelledError:
-            return
-
-        except StanzaError:
-            pass
-
-        self._log.info("Pong")
-
-    def _cancel_ping_task(self) -> None:
-        if self._ping_task is not None:
-            self._ping_task.cancel()
-
     def register_handler(self, *args: Any, **kwargs: Any) -> None:
         self._dispatcher.register_handler(*args, **kwargs)
 
@@ -1140,9 +1162,11 @@
     def destroy(self) -> None:
         for task in self._tasks:
             task.cancel()
-        self._remove_ping_timer()
         self._smacks = None
         self._sasl = None
         self._dispatcher.cleanup()
         self._dispatcher = None
         self.remove_subscriptions()
+        self._remove_stream_end_timeout()
+        self._remove_connection_check_timeout()
+        GLib.source_remove(self._connection_check_id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/dispatcher.py 
new/python-nbxmpp-7.4.0/nbxmpp/dispatcher.py
--- old/python-nbxmpp-7.2.0/nbxmpp/dispatcher.py        2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/dispatcher.py        2026-08-26 
18:54:51.000000000 +0200
@@ -459,7 +459,13 @@
         data = self.replace_non_character(data)
 
         if self._client.is_websocket:
-            stanza = Node(node=data)
+            try:
+                stanza = Node(node=data)
+            except (ExpatError, ValueError) as error:
+                self._log.error("XML parsing error: %s", error)
+                self.notify("parsing-error", str(error))
+                return
+
             if is_websocket_stream_error(stanza):
                 for tag in stanza.getChildren():
                     name = tag.getName()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/account_invite.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/account_invite.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/account_invite.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/account_invite.py    2026-08-26 
18:54:51.000000000 +0200
@@ -0,0 +1,52 @@
+# Copyright (C) 2026 Philipp Hörist <philipp AT hoerist.com>
+#
+# This file is part of nbxmpp.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+import datetime as dt
+
+from nbxmpp.modules.dataforms import SimpleDataForm
+from nbxmpp.modules.date_and_time import parse_datetime
+from nbxmpp.protocol import Node
+from nbxmpp.structs import AccountInviteResult
+
+
+def parse_account_invite(form: Node) -> AccountInviteResult:
+    form = SimpleDataForm(extend=form)
+    assert isinstance(form, SimpleDataForm)
+    # TODO: Check form type once its standardized
+    # field = form.vars.get("FORM_TYPE")
+    # if field is None:
+    #     return None
+
+    try:
+        uri = form["uri"].value
+    except Exception:
+        raise ValueError("uri field is missing")
+
+    assert isinstance(uri, str)
+    if not uri:
+        raise ValueError("empty uri value")
+
+    try:
+        landing_url = form["landing-url"].value or None
+    except Exception:
+        landing_url = None
+
+    assert isinstance(landing_url, str | None)
+
+    try:
+        expire = parse_datetime(form["expire"].value)
+    except Exception:
+        expire = None
+
+    assert isinstance(expire, dt.datetime | None)
+
+    return AccountInviteResult(
+        uri=uri,
+        landing_url=landing_url,
+        expire=expire,
+    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/eme.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/eme.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/eme.py       2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/eme.py       2026-08-26 
18:54:51.000000000 +0200
@@ -40,7 +40,7 @@
         if encryption is None:
             return
 
-        name = encryption.getAttr("name")
+        name = encryption.getAttr("name") or ""
         namespace = encryption.getAttr("namespace")
         if namespace is None:
             self._log.warning("No namespace on message")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/fallback.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/fallback.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/fallback.py  2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/fallback.py  2026-08-26 
18:54:51.000000000 +0200
@@ -16,6 +16,13 @@
     start: int
     end: int
 
+    def __post_init__(self):
+        if self.start < 0 or self.end < 0:
+            raise ValueError("Range with negative numbers: %s", self)
+
+        if self.start >= self.end:
+            raise ValueError("Range with start > end: %s", self)
+
 
 FallbackLangMapT = dict[str | None, FallbackRange | None]
 FallbacksForT = dict[str, FallbackLangMapT | None]
@@ -61,12 +68,8 @@
                 assert end is not None
                 try:
                     range_ = FallbackRange(start=int(start), end=int(end))
-                except Exception:
-                    log.warning("Incorrect range on fallback indication")
-                    return
-
-                if range_.start < 0 or range_.end < 0:
-                    log.warning("Fallback range with negative numbers: %s", 
range_)
+                except Exception as error:
+                    log.warning("Incorrect range on fallback indication: %s", 
error)
                     return
 
             fallback_lang_map[lang] = range_
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/mds.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/mds.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/mds.py       2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/mds.py       2026-08-26 
18:54:51.000000000 +0200
@@ -32,7 +32,7 @@
     XEP-0490
     """
 
-    _depends = {"publish": "PubSub"}
+    _depends = {"publish": "PubSub", "request_items": "PubSub"}
 
     def __init__(self, client: Client) -> None:
         BaseModule.__init__(self, client)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/omemo.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/omemo.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/omemo.py     2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/omemo.py     2026-08-26 
18:54:51.000000000 +0200
@@ -275,7 +275,9 @@
         raise MalformedStanzaError("No bundle node found", item)
 
     result = {}
-    signed_prekey_node = bundle.getTag("signedPreKeyPublic")
+    signed_prekey_node = bundle.getTag(
+        "signedPreKeyPublic", namespace=Namespace.OMEMO_TEMP
+    )
     try:
         result["spk"] = {"key": b64decode(signed_prekey_node.getData())}
     except Exception as error:
@@ -288,26 +290,32 @@
     except Exception as error:
         raise MalformedStanzaError("Invalid signedPreKeyId: %s" % error, item)
 
-    signed_signature_node = bundle.getTag("signedPreKeySignature")
+    signed_signature_node = bundle.getTag(
+        "signedPreKeySignature", namespace=Namespace.OMEMO_TEMP
+    )
     try:
         result["spk_signature"] = b64decode(signed_signature_node.getData())
     except Exception as error:
         error = "Failed to decode signedPreKeySignature: %s" % error
         raise MalformedStanzaError(error, item)
 
-    identity_key_node = bundle.getTag("identityKey")
+    identity_key_node = bundle.getTag("identityKey", 
namespace=Namespace.OMEMO_TEMP)
     try:
         result["ik"] = b64decode(identity_key_node.getData())
     except Exception as error:
         error = "Failed to decode IdentityKey: %s" % error
         raise MalformedStanzaError(error, item)
 
-    prekeys = bundle.getTag("prekeys")
-    if prekeys is None or not prekeys.getChildren():
+    prekeys_node = bundle.getTag("prekeys", namespace=Namespace.OMEMO_TEMP)
+    if prekeys_node is None:
         raise MalformedStanzaError("No prekeys node found", item)
 
+    prekeys = prekeys_node.getTags("preKeyPublic", 
namespace=Namespace.OMEMO_TEMP)
+    if not prekeys:
+        raise MalformedStanzaError("No prekeys found", item)
+
     result["otpks"] = []
-    for prekey in prekeys.getChildren():
+    for prekey in prekeys:
         try:
             id_ = int(prekey.getAttr("preKeyId"))
         except Exception as error:
@@ -400,15 +408,16 @@
     if list_node is None:
         raise MalformedStanzaError("No list node found", item)
 
-    if not list_node.getChildren():
-        return []
-
     result: list[int] = []
-    devices_nodes = list_node.getChildren()
-    for dn in devices_nodes:
+    for dn in list_node.getTags("device", namespace=Namespace.OMEMO_TEMP):
         _id = dn.getAttr("id")
-        if _id:
+        if _id is None:
+            raise MalformedStanzaError("Device node without id attribute", 
item)
+
+        try:
             result.append(int(_id))
+        except Exception:
+            raise MalformedStanzaError("Non integer device id", item)
 
     return result
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/openpgp.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/openpgp.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/openpgp.py   2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/openpgp.py   2026-08-26 
18:54:51.000000000 +0200
@@ -9,7 +9,7 @@
 from typing import Any
 from typing import TYPE_CHECKING
 
-import random
+import secrets
 import string
 import time
 
@@ -344,12 +344,12 @@
 
 
 def get_rpad() -> str:
-    rpad_range = random.randint(30, 50)
-    return "".join(random.choice(string.ascii_letters) for _ in 
range(rpad_range))
+    rpad_range = secrets.randbelow(21) + 30
+    return "".join(secrets.choice(string.ascii_letters) for _ in 
range(rpad_range))
 
 
 def create_message_stanza(
-    stanza: Message, encrypted_payload: str | bytes, with_fallback_text: bool
+    stanza: Message, encrypted_payload: str | bytes, with_eme: bool
 ) -> None:
     b64encoded_payload = b64encode(encrypted_payload)
 
@@ -357,13 +357,11 @@
     openpgp_node.addData(b64encoded_payload)
     stanza.addChild(node=openpgp_node)
 
-    eme_node = Node(
-        "encryption", attrs={"xmlns": Namespace.EME, "namespace": 
Namespace.OPENPGP}
-    )
-    stanza.addChild(node=eme_node)
-
-    if with_fallback_text:
-        stanza.setBody("This message is *encrypted* with OpenPGP")
+    if with_eme:
+        eme_node = Node(
+            "encryption", attrs={"xmlns": Namespace.EME, "namespace": 
Namespace.OPENPGP}
+        )
+        stanza.addChild(node=eme_node)
 
 
 def _make_keylist(keylist: list[PGPKeyMetadata] | None) -> Node:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/vcard_avatar.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/vcard_avatar.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/vcard_avatar.py      2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/vcard_avatar.py      2026-08-26 
18:54:51.000000000 +0200
@@ -15,6 +15,7 @@
 from nbxmpp.protocol import Presence
 from nbxmpp.structs import PresenceProperties
 from nbxmpp.structs import StanzaHandler
+from nbxmpp.util import normalize_sha1
 
 if TYPE_CHECKING:
     from nbxmpp.client import Client
@@ -47,15 +48,27 @@
         avatar_sha = update.getTagData("photo")
         if avatar_sha is None:
             properties.avatar_state = AvatarState.NOT_READY
-            self._log.info("%s is not ready to promote an avatar", 
stanza.getFrom())
+            self._log.info("%s is not ready to promote an avatar", 
properties.jid)
             # Empty update element, ignore
             return
 
         if avatar_sha == "":
             properties.avatar_state = AvatarState.EMPTY
-            self._log.info("%s empty avatar advertised", stanza.getFrom())
+            self._log.info("%s empty avatar advertised", properties.jid)
+            return
+
+        # XEP-0153 hashes are hex SHA-1 of the image bytes. Implementations 
must
+        # accept mixed case and should emit lowercase.
+        normalized_sha = normalize_sha1(avatar_sha)
+        if normalized_sha is None:
+            properties.avatar_state = AvatarState.IGNORE
+            self._log.warning(
+                "%s advertised invalid avatar hash: %r",
+                properties.jid,
+                avatar_sha,
+            )
             return
 
         properties.avatar_sha = avatar_sha
         properties.avatar_state = AvatarState.ADVERTISED
-        self._log.info("%s advertises %s", stanza.getFrom(), avatar_sha)
+        self._log.info("%s advertises %s", properties.jid, avatar_sha)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/modules/vcard_temp.py 
new/python-nbxmpp-7.4.0/nbxmpp/modules/vcard_temp.py
--- old/python-nbxmpp-7.2.0/nbxmpp/modules/vcard_temp.py        2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/modules/vcard_temp.py        2026-08-26 
18:54:51.000000000 +0200
@@ -9,6 +9,7 @@
 from typing import TYPE_CHECKING
 
 import hashlib
+import logging
 from dataclasses import dataclass
 from dataclasses import field
 
@@ -28,6 +29,9 @@
     from nbxmpp.client import Client
 
 
+log = logging.getLogger("nbxmpp.m.vcardtemp")
+
+
 class VCardTemp(BaseModule):
     def __init__(self, client: Client) -> None:
         BaseModule.__init__(self, client)
@@ -135,6 +139,11 @@
         if not avatar:
             return None, None
 
-        encoded_avatar = b64decode(avatar)
-        avatar_sha = hashlib.sha1(encoded_avatar).hexdigest()
-        return encoded_avatar, avatar_sha
+        try:
+            decoded_avatar = b64decode(avatar)
+        except Exception as error:
+            log.warning("Unable to decode avatar: %s", error)
+            return None, None
+
+        avatar_sha = hashlib.sha1(decoded_avatar).hexdigest()
+        return decoded_avatar, avatar_sha
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/protocol.py 
new/python-nbxmpp-7.4.0/nbxmpp/protocol.py
--- old/python-nbxmpp-7.2.0/nbxmpp/protocol.py  2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/protocol.py  2026-08-26 18:54:51.000000000 
+0200
@@ -1054,7 +1054,7 @@
     def isResult(self) -> bool:
         return self.getAttr("type") == "result"
 
-    def getTo(self):
+    def getTo(self) -> JID | None:
         """
         Return value of the 'to' attribute
         """
@@ -1064,7 +1064,7 @@
             pass
         return None
 
-    def getFrom(self):
+    def getFrom(self) -> JID | None:
         """
         Return value of the 'from' attribute
         """
@@ -1270,7 +1270,7 @@
 
     def __init__(
         self,
-        to: str | None = None,
+        to: JID | str | None = None,
         body: str | None = None,
         xhtml: str | None = None,
         typ: Literal["chat", "groupchat", "normal", "headline", "error"] | 
None = None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/smacks.py 
new/python-nbxmpp-7.4.0/nbxmpp/smacks.py
--- old/python-nbxmpp-7.2.0/nbxmpp/smacks.py    2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/smacks.py    2026-08-26 18:54:51.000000000 
+0200
@@ -159,7 +159,7 @@
         self._out_h += 1
 
         if len(self._uqueue) > self.max_queue:
-            self._request_ack()
+            self.request_ack()
         # Send an ack after 100 unacked messages
         if (self._in_h - self._acked_h) > 100:
             self._send_ack()
@@ -244,7 +244,7 @@
         self._log.info("Close session")
         self._reset_state()
 
-    def _request_ack(self) -> None:
+    def request_ack(self) -> None:
         request = Node(Namespace.STREAM_MGMT + " r")
         self._log.debug("Request ack")
         self._client.send_nonza(request, now=False)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/structs.py 
new/python-nbxmpp-7.4.0/nbxmpp/structs.py
--- old/python-nbxmpp-7.2.0/nbxmpp/structs.py   2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/structs.py   2026-08-26 18:54:51.000000000 
+0200
@@ -12,7 +12,7 @@
 from typing import TYPE_CHECKING
 
 import logging
-import random
+import secrets
 import time
 from collections.abc import Sequence
 from dataclasses import dataclass
@@ -235,7 +235,7 @@
     sid: int
     iv: bytes
     keys: dict[int, tuple[bytes, bool]]
-    payload: bytes
+    payload: bytes | None
 
 
 class AnnotationNote(NamedTuple):
@@ -826,7 +826,7 @@
     namespace: str = Namespace.OMEMO_TEMP
 
     def pick_prekey(self) -> dict[str, str]:
-        return random.SystemRandom().choice(self.otpks)
+        return secrets.choice(self.otpks)
 
 
 class ChatMarker(NamedTuple):
@@ -1532,3 +1532,10 @@
         except exceptions.FallbackLanguageError:
             log.warning("Missing fallback for language: %s", lang)
             return text
+
+
+@dataclass
+class AccountInviteResult:
+    uri: str
+    landing_url: str | None
+    expire: datetime | None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/tcp.py 
new/python-nbxmpp-7.4.0/nbxmpp/tcp.py
--- old/python-nbxmpp-7.2.0/nbxmpp/tcp.py       2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/tcp.py       2026-08-26 18:54:51.000000000 
+0200
@@ -53,8 +53,6 @@
         self._input_closed = False
         self._output_closed = False
 
-        self._keepalive_id: int | None = None
-
     def connect(self) -> None:
         self.state = TCPState.CONNECTING
 
@@ -158,25 +156,6 @@
         self.notify("connected")
         self._read_async()
 
-    def _remove_keepalive_timer(self) -> None:
-        if self._keepalive_id is not None:
-            self._log.info("Remove keepalive timer")
-            GLib.source_remove(self._keepalive_id)
-            self._keepalive_id = None
-
-    def _renew_keepalive_timer(self) -> None:
-        if self._con is None:
-            return
-        self._remove_keepalive_timer()
-        self._log.info("Add keepalive timer")
-        self._keepalive_id = GLib.timeout_add_seconds(5, self._send_keepalive)
-
-    def _send_keepalive(self) -> None:
-        self._log.info("Send keepalive")
-        self._keepalive_id = None
-        if not self._con.get_output_stream().has_pending():
-            self._write_all_async(b" ")
-
     def start_tls_negotiation(self) -> None:
         self._log.info("Start TLS negotiation")
         self._tls_handshake_in_progress = True
@@ -254,8 +233,6 @@
             self._finalize("disconnected")
             return
 
-        self._renew_keepalive_timer()
-
         self._read_buffer += data
 
         try:
@@ -321,16 +298,11 @@
         decoded_data = data.decode()
         self._log_stanza(decoded_data, received=False)
 
-        if decoded_data == " ":
-            # keepalive whitespace
-            self._renew_keepalive_timer()
-
-        else:
-            for stanza in self._write_stanza_buffer:
-                try:
-                    self.notify("data-sent", stanza)
-                except Exception:
-                    self._log.exception("Error while executing data-sent:")
+        for stanza in self._write_stanza_buffer:
+            try:
+                self.notify("data-sent", stanza)
+            except Exception:
+                self._log.exception("Error while executing data-sent:")
 
         if self._output_closed and not self._write_queue:
             self._check_for_shutdown()
@@ -353,7 +325,6 @@
             self._write_stanzas()
 
     def disconnect(self) -> None:
-        self._remove_keepalive_timer()
         if self.state == TCPState.CONNECTING:
             self.state = TCPState.DISCONNECTING
             self._connect_cancellable.cancel()
@@ -371,20 +342,17 @@
             self._finalize("disconnected")
 
     def shutdown_input(self) -> None:
-        self._remove_keepalive_timer()
         self._log.info("Shutdown input")
         self._input_closed = True
         self._read_cancellable.cancel()
         self._check_for_shutdown()
 
     def shutdown_output(self) -> None:
-        self._remove_keepalive_timer()
         self.state = TCPState.DISCONNECTING
         self._log.info("Shutdown output")
         self._output_closed = True
 
     def _finalize(self, signal_name: str) -> None:
-        self._remove_keepalive_timer()
         if self._con is not None:
             try:
                 self._con.get_socket().shutdown(True, True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/third_party/hsluv.py 
new/python-nbxmpp-7.4.0/nbxmpp/third_party/hsluv.py
--- old/python-nbxmpp-7.2.0/nbxmpp/third_party/hsluv.py 2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/third_party/hsluv.py 2026-08-26 
18:54:51.000000000 +0200
@@ -6,51 +6,63 @@
 
 import math
 
-m = [
+ColorT = tuple[float, float, float]
+
+# XYZ-to-sRGB matrix
+_m = [
     [3.240969941904521, -1.537383177570093, -0.498610760293],
     [-0.96924363628087, 1.87596750150772, 0.041555057407175],
     [0.055630079696993, -0.20397695888897, 1.056971514242878],
 ]
-minv = [
+# sRGB-to-XYZ matrix
+_m_inv = [
     [0.41239079926595, 0.35758433938387, 0.18048078840183],
     [0.21263900587151, 0.71516867876775, 0.072192315360733],
     [0.019330818715591, 0.11919477979462, 0.95053215224966],
 ]
-REF_Y = 1.0
-REF_U = 0.19783000664283
-REF_V = 0.46831999493879
-KAPPA = 903.2962962
-EPSILON = 0.0088564516
-HEX_CHARS = "0123456789abcdef"
+_ref_y = 1.0
+_ref_u = 0.19783000664283
+_ref_v = 0.46831999493879
+_kappa = 903.2962962  # 24389/27 == (29/3)**3
+_epsilon = 0.0088564516  # 216/24389 == (6/29)**3
+
+
+def _normalize_output(_hx_tuple: ColorT) -> ColorT:
+    # as in snapshot rev 4, the tolerance should be 1e-11
+    return (
+        round(_hx_tuple[0], ndigits=11 - 1),
+        round(_hx_tuple[1], ndigits=11 - 1),
+        round(_hx_tuple[2], ndigits=11 - 1),
+    )
 
 
-def _distance_line_from_origin(line):
-    v = math.pow(line["slope"], 2) + 1
-    return math.fabs(line["intercept"]) / math.sqrt(v)
+def _distance_line_from_origin(line: dict[str, float]) -> float:
+    v = line["slope"] ** 2 + 1
+    return abs(line["intercept"]) / math.sqrt(v)
 
 
-def _length_of_ray_until_intersect(theta, line):
+def _length_of_ray_until_intersect(theta: float, line: dict[str, float]) -> 
float:
     return line["intercept"] / (math.sin(theta) - line["slope"] * 
math.cos(theta))
 
 
-def _get_bounds(l):
-    result = []
-    sub1 = math.pow(l + 16, 3) / 1560896
-    if sub1 > EPSILON:
+def _get_bounds(l: float) -> list[dict[str, float]]:
+    result: list[dict[str, float]] = []
+    sub1 = ((l + 16) ** 3) / 1560896
+    if sub1 > _epsilon:
         sub2 = sub1
     else:
-        sub2 = l / KAPPA
+        sub2 = l / _kappa
     _g = 0
     while _g < 3:
         c = _g
-        _g = _g + 1
-        m1 = m[c][0]
-        m2 = m[c][1]
-        m3 = m[c][2]
+        _g += 1
+        m1 = _m[c][0]
+        m2 = _m[c][1]
+        m3 = _m[c][2]
         _g1 = 0
         while _g1 < 2:
             t = _g1
-            _g1 = _g1 + 1
+            _g1 += 1
             top1 = (284517 * m1 - 94839 * m3) * sub2
             top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * l * sub2 - (
                 769860 * t
@@ -60,282 +72,237 @@
     return result
 
 
-def _max_safe_chroma_for_l(l):
-    bounds = _get_bounds(l)
-    _hx_min = 1.7976931348623157e308
-    _g = 0
-    while _g < 2:
-        i = _g
-        _g = _g + 1
-        length = _distance_line_from_origin(bounds[i])
-        if math.isnan(_hx_min):
-            pass
-        elif math.isnan(length):
-            _hx_min = length
-        else:
-            _hx_min = min(_hx_min, length)
-    return _hx_min
-
-
-def _max_chroma_for_lh(l, h):
-    hrad = h / 360 * math.pi * 2
-    bounds = _get_bounds(l)
-    _hx_min = 1.7976931348623157e308
-    _g = 0
-    while _g < len(bounds):
-        bound = bounds[_g]
-        _g = _g + 1
-        length = _length_of_ray_until_intersect(hrad, bound)
-        if length >= 0:
-            if math.isnan(_hx_min):
-                pass
-            elif math.isnan(length):
-                _hx_min = length
-            else:
-                _hx_min = min(_hx_min, length)
-    return _hx_min
-
-
-def _dot_product(a, b):
-    sum_ = 0
-    _g1 = 0
-    _g = len(a)
-    while _g1 < _g:
-        i = _g1
-        _g1 = _g1 + 1
-        sum_ += a[i] * b[i]
-    return sum_
+def _max_safe_chroma_for_l(l: float) -> float:
+    return min(_distance_line_from_origin(bound) for bound in _get_bounds(l))
+
+
+def _max_chroma_for_lh(l: float, h: float) -> float:
+    hrad = math.radians(h)
+    lengths = [_length_of_ray_until_intersect(hrad, bound) for bound in 
_get_bounds(l)]
+    return min(length for length in lengths if length >= 0)
+
+
+def _dot_product(a: list[float], b: ColorT) -> float:
+    return sum(i * j for i, j in zip(a, b, strict=True))
 
 
-def _from_linear(c):
+def _from_linear(c: float) -> float:
     if c <= 0.0031308:
         return 12.92 * c
-    return 1.055 * math.pow(c, 0.416666666666666685) - 0.055
 
+    return 1.055 * math.pow(c, 5 / 12) - 0.055
 
-def _to_linear(c):
+
+def _to_linear(c: float) -> float:
     if c > 0.04045:
         return math.pow((c + 0.055) / 1.055, 2.4)
+
     return c / 12.92
 
 
-def xyz_to_rgb(_hx_tuple):
-    return [
-        _from_linear(_dot_product(m[0], _hx_tuple)),
-        _from_linear(_dot_product(m[1], _hx_tuple)),
-        _from_linear(_dot_product(m[2], _hx_tuple)),
-    ]
+def _y_to_l(y: float) -> float:
+    if y <= _epsilon:
+        return y / _ref_y * _kappa
 
+    return 116 * math.pow(y / _ref_y, 1 / 3) - 16
 
-def rgb_to_xyz(_hx_tuple):
-    rgbl = [
-        _to_linear(_hx_tuple[0]),
-        _to_linear(_hx_tuple[1]),
-        _to_linear(_hx_tuple[2]),
-    ]
-    return [
-        _dot_product(minv[0], rgbl),
-        _dot_product(minv[1], rgbl),
-        _dot_product(minv[2], rgbl),
-    ]
 
+def _l_to_y(l: float) -> float:
+    if l <= 8:
+        return _ref_y * l / _kappa
 
-def _y_to_l(y):
-    if y <= EPSILON:
-        return y / REF_Y * KAPPA
-    return 116 * math.pow(y / REF_Y, 0.333333333333333315) - 16
+    return _ref_y * (((l + 16) / 116) ** 3)
 
 
-def _l_to_y(l):
-    if l <= 8:
-        return REF_Y * l / KAPPA
-    return REF_Y * math.pow((l + 16) / 116, 3)
+def xyz_to_rgb(_hx_tuple: ColorT) -> ColorT:
+    return (
+        _from_linear(_dot_product(_m[0], _hx_tuple)),
+        _from_linear(_dot_product(_m[1], _hx_tuple)),
+        _from_linear(_dot_product(_m[2], _hx_tuple)),
+    )
 
 
-def xyz_to_luv(_hx_tuple):
+def rgb_to_xyz(_hx_tuple: ColorT) -> ColorT:
+    rgbl = (
+        _to_linear(_hx_tuple[0]),
+        _to_linear(_hx_tuple[1]),
+        _to_linear(_hx_tuple[2]),
+    )
+    return (
+        _dot_product(_m_inv[0], rgbl),
+        _dot_product(_m_inv[1], rgbl),
+        _dot_product(_m_inv[2], rgbl),
+    )
+
+
+def xyz_to_luv(_hx_tuple: ColorT) -> ColorT:
     x = float(_hx_tuple[0])
     y = float(_hx_tuple[1])
     z = float(_hx_tuple[2])
-    divider = x + 15 * y + 3 * z
-    var_u = 4 * x
-    var_v = 9 * y
-    if divider != 0:
-        var_u = var_u / divider
-        var_v = var_v / divider
-    else:
-        var_u = float("nan")
-        var_v = float("nan")
     l = _y_to_l(y)
     if l == 0:
-        return [0, 0, 0]
-    u = 13 * l * (var_u - REF_U)
-    v = 13 * l * (var_v - REF_V)
-    return [l, u, v]
+        return (0, 0, 0)
+    divider = x + 15 * y + 3 * z
+    if divider == 0:
+        u = v = float("nan")
+        return (l, u, v)
+    var_u = 4 * x / divider
+    var_v = 9 * y / divider
+    u = 13 * l * (var_u - _ref_u)
+    v = 13 * l * (var_v - _ref_v)
+    return (l, u, v)
 
 
-def luv_to_xyz(_hx_tuple):
+def luv_to_xyz(_hx_tuple: ColorT) -> ColorT:
     l = float(_hx_tuple[0])
     u = float(_hx_tuple[1])
     v = float(_hx_tuple[2])
     if l == 0:
-        return [0, 0, 0]
-    var_u = u / (13 * l) + REF_U
-    var_v = v / (13 * l) + REF_V
+        return (0, 0, 0)
+    var_u = u / (13 * l) + _ref_u
+    var_v = v / (13 * l) + _ref_v
     y = _l_to_y(l)
-    x = 0 - ((9 * y * var_u) / (((var_u - 4) * var_v) - var_u * var_v))
-    z = (((9 * y) - (15 * var_v * y)) - (var_v * x)) / (3 * var_v)
-    return [x, y, z]
+    x = y * 9 * var_u / (4 * var_v)
+    z = y * (12 - 3 * var_u - 20 * var_v) / (4 * var_v)
+    return (x, y, z)
 
 
-def luv_to_lch(_hx_tuple):
+def luv_to_lch(_hx_tuple: ColorT) -> ColorT:
     l = float(_hx_tuple[0])
     u = float(_hx_tuple[1])
     v = float(_hx_tuple[2])
-    _v = (u * u) + (v * v)
-    if _v < 0:
-        c = float("nan")
-    else:
-        c = math.sqrt(_v)
-    if c < 0.00000001:
+    c = math.hypot(u, v)
+    if c < 1e-08:
         h = 0
     else:
         hrad = math.atan2(v, u)
-        h = hrad * 180.0 / 3.1415926535897932
+        h = math.degrees(hrad)
         if h < 0:
-            h = 360 + h
-    return [l, c, h]
+            h += 360
+    return (l, c, h)
 
 
-def lch_to_luv(_hx_tuple):
+def lch_to_luv(_hx_tuple: ColorT) -> ColorT:
     l = float(_hx_tuple[0])
     c = float(_hx_tuple[1])
     h = float(_hx_tuple[2])
-    hrad = h / 360.0 * 2 * math.pi
+    hrad = math.radians(h)
     u = math.cos(hrad) * c
     v = math.sin(hrad) * c
-    return [l, u, v]
+    return (l, u, v)
 
 
-def hsluv_to_lch(_hx_tuple):
+def hsluv_to_lch(_hx_tuple: ColorT) -> ColorT:
     h = float(_hx_tuple[0])
     s = float(_hx_tuple[1])
     l = float(_hx_tuple[2])
-    if l > 99.9999999:
-        return [100, 0, h]
-    if l < 0.00000001:
-        return [0, 0, h]
+    if l > 100 - 1e-7:
+        return (100, 0, h)
+    if l < 1e-08:
+        return (0, 0, h)
     _hx_max = _max_chroma_for_lh(l, h)
     c = _hx_max / 100 * s
-    return [l, c, h]
+    return (l, c, h)
 
 
-def lch_to_hsluv(_hx_tuple):
+def lch_to_hsluv(_hx_tuple: ColorT) -> ColorT:
     l = float(_hx_tuple[0])
     c = float(_hx_tuple[1])
     h = float(_hx_tuple[2])
-    if l > 99.9999999:
-        return [h, 0, 100]
-    if l < 0.00000001:
-        return [h, 0, 0]
+    if l > 100 - 1e-7:
+        return (h, 0, 100)
+    if l < 1e-08:
+        return (h, 0, 0)
     _hx_max = _max_chroma_for_lh(l, h)
     s = c / _hx_max * 100
-    return [h, s, l]
+    return (h, s, l)
 
 
-def hpluv_to_lch(_hx_tuple):
+def hpluv_to_lch(_hx_tuple: ColorT) -> ColorT:
     h = float(_hx_tuple[0])
     s = float(_hx_tuple[1])
     l = float(_hx_tuple[2])
-    if l > 99.9999999:
-        return [100, 0, h]
-    if l < 0.00000001:
-        return [0, 0, h]
+    if l > 100 - 1e-7:
+        return (100, 0, h)
+    if l < 1e-08:
+        return (0, 0, h)
     _hx_max = _max_safe_chroma_for_l(l)
     c = _hx_max / 100 * s
-    return [l, c, h]
+    return (l, c, h)
 
 
-def lch_to_hpluv(_hx_tuple):
+def lch_to_hpluv(_hx_tuple: ColorT) -> ColorT:
     l = float(_hx_tuple[0])
     c = float(_hx_tuple[1])
     h = float(_hx_tuple[2])
-    if l > 99.9999999:
-        return [h, 0, 100]
-    if l < 0.00000001:
-        return [h, 0, 0]
+    if l > 100 - 1e-7:
+        return (h, 0, 100)
+    if l < 1e-08:
+        return (h, 0, 0)
     _hx_max = _max_safe_chroma_for_l(l)
     s = c / _hx_max * 100
-    return [h, s, l]
+    return (h, s, l)
 
 
-def rgb_to_hex(_hx_tuple):
-    h = "#"
-    _g = 0
-    while _g < 3:
-        i = _g
-        _g = _g + 1
-        chan = float(_hx_tuple[i])
-        c = math.floor(chan * 255 + 0.5)
-        digit2 = int(c % 16)
-        digit1 = int((c - digit2) / 16)
-
-        h += HEX_CHARS[digit1] + HEX_CHARS[digit2]
-    return h
+def rgb_to_hex(_hx_tuple: ColorT) -> str:
+    return "#{:02x}{:02x}{:02x}".format(
+        math.floor(_hx_tuple[0] * 255 + 0.5),
+        math.floor(_hx_tuple[1] * 255 + 0.5),
+        math.floor(_hx_tuple[2] * 255 + 0.5),
+    )
 
 
-def hex_to_rgb(hex_):
-    hex_ = hex_.lower()
-    ret = []
-    _g = 0
-    while _g < 3:
-        i = _g
-        _g = _g + 1
-        index = i * 2 + 1
-        _hx_str = hex_[index]
-        digit1 = HEX_CHARS.find(_hx_str)
-        index1 = i * 2 + 2
-        str1 = hex_[index1]
-        digit2 = HEX_CHARS.find(str1)
-        n = digit1 * 16 + digit2
-        ret.append(n / 255.0)
-    return ret
+def hex_to_rgb(_hex: str) -> ColorT:
+    # skip leading '#'
+    r = int(_hex[1:3], base=16) / 255.0
+    g = int(_hex[3:5], base=16) / 255.0
+    b = int(_hex[5:7], base=16) / 255.0
+    return (r, g, b)
 
 
-def lch_to_rgb(_hx_tuple):
+def lch_to_rgb(_hx_tuple: ColorT) -> ColorT:
     return xyz_to_rgb(luv_to_xyz(lch_to_luv(_hx_tuple)))
 
 
-def rgb_to_lch(_hx_tuple):
+def rgb_to_lch(_hx_tuple: ColorT) -> ColorT:
     return luv_to_lch(xyz_to_luv(rgb_to_xyz(_hx_tuple)))
 
 
-def hsluv_to_rgb(_hx_tuple):
+def _hsluv_to_rgb(_hx_tuple: ColorT) -> ColorT:
     return lch_to_rgb(hsluv_to_lch(_hx_tuple))
 
 
-def rgb_to_hsluv(_hx_tuple):
+def hsluv_to_rgb(_hx_tuple: ColorT) -> ColorT:
+    return _normalize_output(_hsluv_to_rgb(_hx_tuple))
+
+
+def rgb_to_hsluv(_hx_tuple: ColorT) -> ColorT:
     return lch_to_hsluv(rgb_to_lch(_hx_tuple))
 
 
-def hpluv_to_rgb(_hx_tuple):
+def _hpluv_to_rgb(_hx_tuple: ColorT) -> ColorT:
     return lch_to_rgb(hpluv_to_lch(_hx_tuple))
 
 
-def rgb_to_hpluv(_hx_tuple):
+def hpluv_to_rgb(_hx_tuple: ColorT) -> ColorT:
+    return _normalize_output(_hpluv_to_rgb(_hx_tuple))
+
+
+def rgb_to_hpluv(_hx_tuple: ColorT) -> ColorT:
     return lch_to_hpluv(rgb_to_lch(_hx_tuple))
 
 
-def hsluv_to_hex(_hx_tuple):
+def hsluv_to_hex(_hx_tuple: ColorT) -> str:
     return rgb_to_hex(hsluv_to_rgb(_hx_tuple))
 
 
-def hpluv_to_hex(_hx_tuple):
+def hpluv_to_hex(_hx_tuple: ColorT) -> str:
     return rgb_to_hex(hpluv_to_rgb(_hx_tuple))
 
 
-def hex_to_hsluv(s):
+def hex_to_hsluv(s: str) -> ColorT:
     return rgb_to_hsluv(hex_to_rgb(s))
 
 
-def hex_to_hpluv(s):
+def hex_to_hpluv(s: str) -> ColorT:
     return rgb_to_hpluv(hex_to_rgb(s))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/nbxmpp/util.py 
new/python-nbxmpp-7.4.0/nbxmpp/util.py
--- old/python-nbxmpp-7.2.0/nbxmpp/util.py      2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/nbxmpp/util.py      2026-08-26 18:54:51.000000000 
+0200
@@ -53,6 +53,8 @@
 
 log = logging.getLogger("nbxmpp.util")
 
+SHA1_RX = re.compile(r"[a-f0-9]{40}")
+
 
 def b64decode(data: str | bytes) -> bytes:
     if not data:
@@ -61,6 +63,8 @@
     if isinstance(data, str):
         data = data.encode()
 
+    # Don't validate base64 data because in XMPP whitespace in
+    # base64 payloads must be accepted
     return base64.b64decode(data)
 
 
@@ -139,7 +143,7 @@
     saturation: float,
     lightness: float,
 ) -> tuple[float, float, float]:
-    return clip_rgb(*hsluv.hsluv_to_rgb((hue, saturation, lightness)))  # 
type: ignore
+    return clip_rgb(*hsluv.hsluv_to_rgb((hue, saturation, lightness)))
 
 
 @lru_cache(maxsize=1024)
@@ -473,3 +477,11 @@
                 raise ValueError("No href attr found")
             return href
     raise ValueError("no websocket uri found")
+
+
+def normalize_sha1(value: str) -> str | None:
+    """Return a canonical lowercase SHA-1 hex digest, or None if invalid."""
+    value = value.lower()
+    if SHA1_RX.fullmatch(value) is None:
+        return None
+    return value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/pyproject.toml 
new/python-nbxmpp-7.4.0/pyproject.toml
--- old/python-nbxmpp-7.2.0/pyproject.toml      2026-04-19 19:12:23.000000000 
+0200
+++ new/python-nbxmpp-7.4.0/pyproject.toml      2026-08-26 18:54:51.000000000 
+0200
@@ -8,7 +8,7 @@
 keywords = ["chat", "messaging", "im", "xmpp"]
 readme = "README.md"
 requires-python = ">=3.10"
-license = {text = "GPL-3.0-or-later"}
+license = "GPL-3.0-or-later"
 authors = [
     {name = "Philipp Hörist", email = "[email protected]"},
     {name = "Yann Leboulanger", email = "[email protected]"},
@@ -219,7 +219,6 @@
   "S112",   # try`-`except`-`continue`
   "S303",   # Use of insecure MD2, MD4, MD5, or SHA1 hash function
   "S310",   # Audit URL open for permitted schemes
-  "S311",   # Standard pseudo-random generators are not suitable for 
cryptographic purposes
   "S314",   # Using `xml` to parse untrusted data is known to be vulnerable to 
XML attacks; use `defusedxml` equivalents
   "S324",   # Probable use of insecure hash functions
   "S608",   # Possible SQL injection vector through string-based query 
construction
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/test/unit/test_fallback.py 
new/python-nbxmpp-7.4.0/test/unit/test_fallback.py
--- old/python-nbxmpp-7.2.0/test/unit/test_fallback.py  2026-04-19 
19:12:23.000000000 +0200
+++ new/python-nbxmpp-7.4.0/test/unit/test_fallback.py  2026-08-26 
18:54:51.000000000 +0200
@@ -64,6 +64,20 @@
 
         self.assertIsNone(fallbacks_for)
 
+        xml = """
+            <message to='[email protected]' id='message-id2' type='groupchat'>
+              <body>> Anna wrote:\n> Hi, how are you?\nGreat</body>
+              <fallback xmlns='urn:xmpp:fallback:0' for='urn:xmpp:test:0'>
+                <body start='10' end='10' />
+              </fallback>
+            </message>
+        """
+
+        message = nbxmpp.Message(node=xml)
+        fallbacks_for = parse_fallback_indication(log, message)
+
+        self.assertIsNone(fallbacks_for)
+
     def test_strip_fallback(self):
         fallbacks_for: FallbacksForT = {
             "urn:xmpp:test:1": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-nbxmpp-7.2.0/test/unit/test_omemo.py 
new/python-nbxmpp-7.4.0/test/unit/test_omemo.py
--- old/python-nbxmpp-7.2.0/test/unit/test_omemo.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/python-nbxmpp-7.4.0/test/unit/test_omemo.py     2026-08-26 
18:54:51.000000000 +0200
@@ -0,0 +1,40 @@
+import unittest
+
+from nbxmpp.modules.omemo import _parse_bundle
+from nbxmpp.modules.omemo import _parse_devicelist
+from nbxmpp.simplexml import Node
+
+
+class OMEMOTest(unittest.TestCase):
+
+    def test_parsing(self):
+
+        devicelist = """
+          <item id="current">
+            <list xmlns="eu.siacs.conversations.axolotl">
+              <device id="912561474" />
+              <device id="532656838" />
+              <device id="1" xmlns="test.namespace" />
+            </list>
+          </item>"""
+
+        devices = _parse_devicelist(Node(node=devicelist))
+        self.assertEqual(devices, [912561474, 532656838])
+
+        bundle = """
+          <item id='current'>
+            <bundle xmlns='eu.siacs.conversations.axolotl'>
+              <signedPreKeyPublic 
signedPreKeyId='1'>dGVzdHBheWxvYWQ=</signedPreKeyPublic>
+              <signedPreKeySignature>dGVzdHBheWxvYWQ=</signedPreKeySignature>
+              <identityKey>dGVzdHBheWxvYWQ=</identityKey>
+              <prekeys>
+                <preKeyPublic preKeyId='1'>dGVzdHBheWxvYWQ=</preKeyPublic>
+                <preKeyPublic preKeyId='2'>dGVzdHBheWxvYWQ=</preKeyPublic>
+                <preKeyPublic preKeyId='3'>dGVzdHBheWxvYWQ=</preKeyPublic>
+                <preKeyPublic preKeyId='3' 
xmlns="test.namespace">dGVzdHBheWxvYWQ=</preKeyPublic>
+              </prekeys>
+            </bundle>
+          </item>"""
+
+        omemo_bundle = _parse_bundle(Node(node=bundle), 1)
+        self.assertEqual(len(omemo_bundle.otpks), 3)

Reply via email to