diff --git a/doap.xml b/doap.xml
index c22e64e9..20771694 100644
--- a/doap.xml
+++ b/doap.xml
@@ -909,6 +909,14 @@
no thumbnail support
+
+
+
+ complete
+ 0.1.0
+ 1.8.6
+
+
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index e0c8f38d..eeb65084 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -121,6 +121,7 @@ PLUGINS = [
'xep_0447', # Stateless file sharing
'xep_0461', # Message Replies
'xep_0469', # Bookmarks Pinning
+ 'xep_0490', # Message Displayed Synchronization
# Meant to be imported by plugins
]
diff --git a/slixmpp/plugins/xep_0223.py b/slixmpp/plugins/xep_0223.py
index 6ed39285..2a684299 100644
--- a/slixmpp/plugins/xep_0223.py
+++ b/slixmpp/plugins/xep_0223.py
@@ -20,6 +20,18 @@ 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'
@@ -28,6 +40,7 @@ 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:
"""
@@ -70,7 +83,8 @@ class XEP_0223(BasePlugin):
value='http://jabber.org/protocol/pubsub#publish-options')
fields = options['fields']
- for field, value in self.profile.items():
+ profile = self.profile | self.node_profiles.get(node, {})
+ for field, value in profile.items():
if field not in fields:
options.add_field(var=field)
options.get_fields()[field]['value'] = value
diff --git a/slixmpp/plugins/xep_0490/__init__.py b/slixmpp/plugins/xep_0490/__init__.py
new file mode 100644
index 00000000..2706a307
--- /dev/null
+++ b/slixmpp/plugins/xep_0490/__init__.py
@@ -0,0 +1,8 @@
+from slixmpp.plugins.base import register_plugin
+
+from . import stanza
+from .mds import XEP_0490
+
+register_plugin(XEP_0490)
+
+__all__ = ['stanza', 'XEP_0490']
diff --git a/slixmpp/plugins/xep_0490/mds.py b/slixmpp/plugins/xep_0490/mds.py
new file mode 100644
index 00000000..a4b6f0c7
--- /dev/null
+++ b/slixmpp/plugins/xep_0490/mds.py
@@ -0,0 +1,42 @@
+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
+ )
diff --git a/slixmpp/plugins/xep_0490/stanza.py b/slixmpp/plugins/xep_0490/stanza.py
new file mode 100644
index 00000000..42b50269
--- /dev/null
+++ b/slixmpp/plugins/xep_0490/stanza.py
@@ -0,0 +1,17 @@
+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)
diff --git a/slixmpp/pluginsdict.py b/slixmpp/pluginsdict.py
index 4e5b7b9e..80f6a96a 100644
--- a/slixmpp/pluginsdict.py
+++ b/slixmpp/pluginsdict.py
@@ -103,6 +103,7 @@ 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):
@@ -199,3 +200,4 @@ class PluginsDict(TypedDict):
xep_0439: XEP_0439
xep_0444: XEP_0444
xep_0461: XEP_0461
+ xep_0490: XEP_0490
diff --git a/tests/test_stream_xep_0490.py b/tests/test_stream_xep_0490.py
new file mode 100644
index 00000000..4932a9b3
--- /dev/null
+++ b/tests/test_stream_xep_0490.py
@@ -0,0 +1,135 @@
+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
+ """
+
+
+
+
+
+ """
+ )
+ self.recv( # language=XML
+ """
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
+
+
+ """
+ )
+ 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
+ """
+
+
+
+ -
+
+
+
+
+
+
+
+
+ http://jabber.org/protocol/pubsub#publish-options
+
+
+ 1
+
+
+ max
+
+
+ never
+
+
+ whitelist
+
+
+
+
+
+ """,
+ 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
+ """
+
+
+
+ -
+
+
+
+
+
+
+
+ """
+ )
+ 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)