diff --git a/doap.xml b/doap.xml
index fed71c78..2c766992 100644
--- a/doap.xml
+++ b/doap.xml
@@ -917,6 +917,14 @@
1.8.6
+
+
+
+ complete
+ 0.1.0
+ 1.8.7
+
+
diff --git a/docs/api/plugins/index.rst b/docs/api/plugins/index.rst
index 6737d3c9..8f42f40d 100644
--- a/docs/api/plugins/index.rst
+++ b/docs/api/plugins/index.rst
@@ -94,3 +94,4 @@ Plugin index
xep_0439
xep_0441
xep_0444
+ xep_0492
diff --git a/docs/api/plugins/xep_0492.rst b/docs/api/plugins/xep_0492.rst
new file mode 100644
index 00000000..9940e264
--- /dev/null
+++ b/docs/api/plugins/xep_0492.rst
@@ -0,0 +1,18 @@
+
+XEP-0492: Chat Notification Settings
+===========================
+
+.. module:: slixmpp.plugins.xep_0492
+
+.. autoclass:: XEP_0492
+ :members:
+ :exclude-members: session_bind, plugin_init, plugin_end
+
+
+Stanza elements
+---------------
+
+.. automodule:: slixmpp.plugins.xep_0492.stanza
+ :members:
+ :undoc-members:
+
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index eeb65084..dd43077c 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -122,6 +122,7 @@ PLUGINS = [
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
'xep_0490', # Message Displayed Synchronization
+ 'xep_0492', # Chat Notification Settings
# Meant to be imported by plugins
]
diff --git a/slixmpp/plugins/xep_0492/__init__.py b/slixmpp/plugins/xep_0492/__init__.py
new file mode 100644
index 00000000..0ae4ee1a
--- /dev/null
+++ b/slixmpp/plugins/xep_0492/__init__.py
@@ -0,0 +1,13 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2025 nicoco
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permission.
+
+from slixmpp.plugins.base import register_plugin
+
+from . import stanza
+from .notify import XEP_0492
+
+register_plugin(XEP_0492)
+
+__all__ = ["stanza", "XEP_0492"]
diff --git a/slixmpp/plugins/xep_0492/notify.py b/slixmpp/plugins/xep_0492/notify.py
new file mode 100644
index 00000000..8b6d90a2
--- /dev/null
+++ b/slixmpp/plugins/xep_0492/notify.py
@@ -0,0 +1,21 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2025 nicoco
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permission.
+
+from slixmpp.plugins import BasePlugin
+from . import stanza
+
+
+class XEP_0492(BasePlugin):
+ """
+ XEP-0492: Chat notification settings
+ """
+
+ name = "xep_0492"
+ description = "XEP-0492: Chat notification settings"
+ dependencies = {"xep_0402"}
+ stanza = stanza
+
+ def plugin_init(self):
+ stanza.register_plugin()
diff --git a/slixmpp/plugins/xep_0492/stanza.py b/slixmpp/plugins/xep_0492/stanza.py
new file mode 100644
index 00000000..5beb8588
--- /dev/null
+++ b/slixmpp/plugins/xep_0492/stanza.py
@@ -0,0 +1,106 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2025 nicoco
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permission.
+
+from typing import Literal, Optional, cast
+
+from slixmpp import register_stanza_plugin
+from slixmpp.plugins.xep_0402.stanza import Extensions
+from slixmpp.types import ClientTypes
+from slixmpp.xmlstream import ElementBase
+
+NS = "urn:xmpp:notification-settings:0"
+
+WhenLiteral = Literal["never", "always", "on-mention"]
+
+
+class Notify(ElementBase):
+ """
+ Chat notification settings element
+
+
+ To enable it on a Conference element, use configure() like this:
+
+ .. code-block::python
+
+ # C being a Conference element
+ C['extensions']["notify"].configure("always", client_type="pc")
+
+ Which will add the element to the 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 and a 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)
diff --git a/slixmpp/types.py b/slixmpp/types.py
index cc1e9dc4..7faee291 100644
--- a/slixmpp/types.py
+++ b/slixmpp/types.py
@@ -109,8 +109,21 @@ ErrorConditions = Literal[
"unexpected-request",
]
+# https://xmpp.org/registrar/disco-categories.html#client
+ClientTypes = Literal[
+ "bot",
+ "console",
+ "game",
+ "handheld",
+ "pc",
+ "phone",
+ "sms",
+ "tablet",
+ "web",
+]
+
__all__ = [
'Protocol', 'TypedDict', 'Literal', 'OptJid', 'OptJidStr', 'JidStr', 'MAMDefault',
'PresenceTypes', 'PresenceShows', 'MessageTypes', 'IqTypes', 'MucRole',
- 'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes'
+ 'MucAffiliation', 'FilterString', 'ErrorConditions', 'ErrorTypes', 'ClientTypes'
]
diff --git a/tests/test_stanza_xep_0492.py b/tests/test_stanza_xep_0492.py
new file mode 100644
index 00000000..bd4e6c19
--- /dev/null
+++ b/tests/test_stanza_xep_0492.py
@@ -0,0 +1,178 @@
+# Slixmpp: The Slick XMPP Library
+# Copyright (C) 2025 nicoco
+# This file is part of Slixmpp.
+# See the file LICENSE for copying permission.
+
+import unittest
+
+from slixmpp import register_stanza_plugin, ElementBase
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0492 import stanza
+from slixmpp.plugins.xep_0402 import stanza as b_stanza
+
+
+class TestNotificationSetting(SlixTest):
+ def setUp(self):
+ b_stanza.register_plugin()
+ stanza.register_plugin()
+
+ def test_never(self):
+ bookmark = b_stanza.Conference()
+ bookmark["extensions"]["notify"].configure("never")
+ self.check(
+ bookmark,
+ """
+
+
+
+
+
+
+
+ """,
+ use_values=False,
+ )
+
+ def test_always(self):
+ bookmark = b_stanza.Conference()
+ bookmark["extensions"]["notify"].configure("always")
+ self.check(
+ bookmark,
+ """
+
+
+
+
+
+
+
+ """,
+ use_values=False,
+ )
+
+ def test_on_mention(self):
+ bookmark = b_stanza.Conference()
+ bookmark["extensions"]["notify"].configure("on-mention")
+ self.check(
+ bookmark,
+ """
+
+
+
+
+
+
+
+ """,
+ 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,
+ """
+
+
+
+
+
+
+ cool-content
+
+
+
+
+ """,
+ 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,
+ """
+
+
+
+
+
+
+
+
+
+ """,
+ use_values=False,
+ )
+
+ bookmark["extensions"]["notify"].configure("always")
+
+ self.check(
+ bookmark,
+ """
+
+
+
+
+
+
+
+
+
+ """,
+ use_values=False,
+ )
+
+ bookmark["extensions"]["notify"].configure("always", "mobile")
+
+ self.check(
+ bookmark,
+ """
+
+
+
+
+
+
+
+
+
+ """,
+ 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)