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)