~nicoco/slidge

c645bd5fa84045c73d31f54703d47f975cbd91b2 — nicoco 19 days ago 7f7b1b1
feat: support for Message Displayed Synchronization in MUCs
M slidge/core/gateway/base.py => slidge/core/gateway/base.py +53 -0
@@ 14,6 14,7 @@ import aiohttp
import qrcode
from slixmpp import JID, ComponentXMPP, Iq, Message, Presence
from slixmpp.exceptions import IqError, IqTimeout, XMPPError
from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation
from slixmpp.types import MessageTypes
from slixmpp.xmlstream.xmlstream import NotConnectedError



@@ 384,6 385,7 @@ class BaseGateway(
            #       as last resort.
            try:
                await self["xep_0100"].add_component_to_roster(user.jid)
                await self.__add_component_to_mds_whitelist(user.jid)
            except IqError as e:
                # TODO: remove the user when this happens? or at least
                # this can happen when the user has unsubscribed from the XMPP server


@@ 401,6 403,56 @@ class BaseGateway(

        log.info("Slidge has successfully started")

    async def __add_component_to_mds_whitelist(self, user_jid: JID):
        # Uses privileged entity to add ourselves to the whitelist of the PEP
        # MDS node so we receive MDS events
        iq_creation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set")
        iq_creation["pubsub"]["create"]["node"] = self["mds"].stanza.NS

        try:
            await self["xep_0356"].send_privileged_iq(iq_creation)
        except PermissionError:
            log.warning(
                "IQ privileges not granted for pubsub namespace, we cannot "
                "create the MDS node of %s",
                user_jid,
            )
        except IqError as e:
            # conflict this means the node already exists, we can ignore that
            if e.condition != "conflict":
                log.exception(
                    "Could not create the MDS node of %s", user_jid, exc_info=e
                )
        except Exception as e:
            log.exception(
                "Error while trying to create to the MDS node of %s",
                user_jid,
                exc_info=e,
            )

        iq_affiliation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set")
        iq_affiliation["pubsub_owner"]["affiliations"]["node"] = self["mds"].stanza.NS

        aff = OwnerAffiliation()
        aff["jid"] = self.boundjid.bare
        aff["affiliation"] = "member"
        iq_affiliation["pubsub_owner"]["affiliations"].append(aff)

        try:
            await self["xep_0356"].send_privileged_iq(iq_affiliation)
        except PermissionError:
            log.warning(
                "IQ privileges not granted for pubsub#owner namespace, we cannot "
                "listen to the MDS events of %s",
                user_jid,
            )
        except Exception as e:
            log.exception(
                "Error while trying to subscribe to the MDS node of %s",
                user_jid,
                exc_info=e,
            )

    async def __login_wrap(self, session: "BaseSession"):
        session.send_gateway_status("Logging in…", show="dnd")
        try:


@@ 830,6 882,7 @@ SLIXMPP_PLUGINS = [
    "xep_0444",  # Message reactions
    "xep_0447",  # Stateless File Sharing
    "xep_0461",  # Message replies
    "mds",  # Message Displayed Synchronization
]

LOG_STRIP_ELEMENTS = ["data", "binval"]

M slidge/core/gateway/session_dispatcher.py => slidge/core/gateway/session_dispatcher.py +13 -0
@@ 79,6 79,7 @@ class SessionDispatcher:
            "groupchat_direct_invite",
            "groupchat_subject",
            "avatar_metadata_publish",
            "message_displayed_synchronization_publish",
        ):
            xmpp.add_event_handler(
                event, _exceptions_to_xmpp_errors(getattr(self, "on_" + event))


@@ 476,6 477,18 @@ class SessionDispatcher:
        muc = await session.bookmarks.by_jid(p.get_to())
        await muc.join(p)

    async def on_message_displayed_synchronization_publish(self, msg: Message):
        session = await self.__get_session(msg, timeout=None)

        chat_jid = msg["pubsub_event"]["items"]["item"]["id"]
        chat = await session.get_contact_or_group_or_participant(JID(chat_jid))
        if not isinstance(chat, LegacyMUC):
            session.log.debug("Ignoring non-groupchat MDS event")
            return

        stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"]
        await session.on_displayed(chat, _xmpp_msg_id_to_legacy(session, stanza_id))

    async def on_avatar_metadata_publish(self, m: Message):
        if not config.SYNC_AVATAR:
            return

M slidge/core/mixins/message.py => slidge/core/mixins/message.py +30 -1
@@ 4,8 4,9 @@ import warnings
from datetime import datetime
from typing import TYPE_CHECKING, Iterable, Optional

from slixmpp import Message
from slixmpp import Iq, Message

from ...slixfix.xep_mds.mds import PUBLISH_OPTIONS
from ...util.types import (
    ChatState,
    LegacyMessageType,


@@ 127,6 128,34 @@ class MarkerMixin(MessageMaker):
            self._make_marker(legacy_msg_id, "displayed", carbon=kwargs.get("carbon")),
            **kwargs,
        )
        if getattr(self, "is_user", False):
            self.xmpp.loop.create_task(self.__send_mds(legacy_msg_id))

    async def __send_mds(self, legacy_msg_id: LegacyMessageType):
        # Send a MDS displayed marker on behalf of the user for a group chat
        if muc := getattr(self, "muc", None):
            muc_jid = muc.jid.bare
        else:
            # This is not implemented for 1:1 chat because it would rely on
            # storing the XMPP-server injected stanza-id, which we don't track
            # ATM.
            # In practice, MDS should mostly be useful for public group chats,
            # so it should not be an issue.
            # We'll see if we need to implement that later
            return
        xmpp_msg_id = self._legacy_to_xmpp(legacy_msg_id)
        iq = Iq(sto=self.user.bare_jid, sfrom=self.user.bare_jid)
        iq["pubsub"]["publish"]["node"] = self.xmpp["mds"].stanza.NS
        iq["pubsub"]["publish"]["item"]["id"] = muc_jid
        displayed = self.xmpp["mds"].stanza.Displayed()
        displayed["stanza_id"]["id"] = xmpp_msg_id
        displayed["stanza_id"]["by"] = muc_jid
        iq["pubsub"]["publish"]["item"]["payload"] = displayed
        iq["pubsub"]["publish_options"] = PUBLISH_OPTIONS
        try:
            await self.xmpp["xep_0356"].send_privileged_iq(iq)
        except Exception as e:
            self.session.log.debug("Could not MDS mark", exc_info=e)


class ContentMessageMixin(AttachmentMixin):

M slidge/slixfix/__init__.py => slidge/slixfix/__init__.py +2 -0
@@ 19,6 19,7 @@ from . import (  # xep_0356,
    xep_0317,
    xep_0356_old,
    xep_0424,
    xep_mds,
)




@@ 59,6 60,7 @@ slixmpp.plugins.PLUGINS.extend(
        "xep_0292_provider",
        "xep_0317",
        "xep_0356_old",
        "mds",
    ]
)


A slidge/slixfix/xep_mds/__init__.py => slidge/slixfix/xep_mds/__init__.py +8 -0
@@ 0,0 1,8 @@
from slixmpp.plugins.base import register_plugin

from . import stanza
from .mds import XEP_xxxx_mds

register_plugin(XEP_xxxx_mds)

__all__ = ["stanza", "XEP_xxxx_mds"]

A slidge/slixfix/xep_mds/mds.py => slidge/slixfix/xep_mds/mds.py +47 -0
@@ 0,0 1,47 @@
from slixmpp import Iq
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0004 import Form
from slixmpp.types import JidStr

from . import stanza


class XEP_xxxx_mds(BasePlugin):  # FIXME
    """
    XEP-xxxx: Message Displayed Synchronization
    """

    name = "mds"
    description = "XEP-xxxx: Message Displayed Synchronization"
    dependencies = {"xep_0060", "xep_0163", "xep_0359"}
    stanza = stanza

    def plugin_init(self):
        stanza.register_plugin()
        self.xmpp.plugin["xep_0163"].register_pep(
            "message_displayed_synchronization",
            stanza.Displayed,
        )

    def flag_chat(self, chat: JidStr, stanza_id: str, **kwargs) -> Iq:
        displayed = stanza.Displayed()
        displayed["stanza_id"]["id"] = stanza_id
        return self.xmpp.plugin["xep_0163"].publish(
            displayed, node=stanza.NS, options=PUBLISH_OPTIONS, id=str(chat), **kwargs
        )

    def catch_up(self, **kwargs):
        return self.xmpp.plugin["xep_0060"].get_items(
            self.xmpp.boundjid.bare, stanza.NS, **kwargs
        )


PUBLISH_OPTIONS = Form()
PUBLISH_OPTIONS["type"] = "submit"
PUBLISH_OPTIONS.add_field(
    "FORM_TYPE", "hidden", value="http://jabber.org/protocol/pubsub#publish-options"
)
PUBLISH_OPTIONS.add_field("pubsub#persist_items", value="true")
PUBLISH_OPTIONS.add_field("pubsub#max_items", value="max")
PUBLISH_OPTIONS.add_field("pubsub#send_last_published_item", value="never")
PUBLISH_OPTIONS.add_field("pubsub#access_model", value="whitelist")

A slidge/slixfix/xep_mds/stanza.py => slidge/slixfix/xep_mds/stanza.py +17 -0
@@ 0,0 1,17 @@
from slixmpp import register_stanza_plugin
from slixmpp.plugins.xep_0060.stanza import Item
from slixmpp.plugins.xep_0359.stanza import StanzaID
from slixmpp.xmlstream import ElementBase

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)

A tests/test_mds.py => tests/test_mds.py +190 -0
@@ 0,0 1,190 @@
import unittest.mock

from slixmpp import JID
from slixmpp.plugins.xep_0356.permissions import IqPermission
from test_muc import Base as BaseMUC

# from test_shakespeare import Base as BaseNoMUC


class MDSMixin:
    def setUp(self):
        super().setUp()
        for domain in "test.com", "montague.lit":
            self.xmpp["xep_0356"].granted_privileges[domain].iq[
                "http://jabber.org/protocol/pubsub"
            ] = IqPermission.BOTH
            self.xmpp["xep_0356"].granted_privileges[domain].iq[
                "http://jabber.org/protocol/pubsub#owner"
            ] = IqPermission.BOTH
        patch = unittest.mock.patch("uuid.uuid4", return_value="uuid")
        patch.start()


class TestMDS(MDSMixin, BaseMUC):
    def test_add_to_whitelist(self):
        task = self.xmpp.loop.create_task(
            self.xmpp._BaseGateway__add_component_to_mds_whitelist(JID("test@test.com"))
        )
        self.send(  # language=XML
            """
            <iq id="uuid"
                to="test@test.com"
                from="aim.shakespeare.lit"
                type="set">
              <privileged_iq xmlns="urn:xmpp:privilege:2">
                <iq xmlns="jabber:client"
                    type="set"
                    to="test@test.com"
                    from="test@test.com"
                    id="uuid">
                  <pubsub xmlns="http://jabber.org/protocol/pubsub">
                    <create node="urn:xmpp:mds:displayed:0" />
                  </pubsub>
                </iq>
              </privileged_iq>
            </iq>
            """,
            use_values=False,
        )
        self.recv(  # language=XML
            """
            <iq id="uuid"
                type="result"
                to="test@test.com"
                from="aim.shakespeare.lit">
              <privilege xmlns="urn:xmpp:privilege:2">
                <forwarded xmlns="urn:xmpp:forward:0">
                  <iq xmlns="jabber:client"
                      id="uuid"
                      type="result"
                      to="test@localhost" />
                </forwarded>
              </privilege>
            </iq>
            """
        )
        self.send(  # language=XML
            """
            <iq id="uuid"
                to="test@test.com"
                from="aim.shakespeare.lit"
                type="set">
              <privileged_iq xmlns="urn:xmpp:privilege:2">
                <iq xmlns="jabber:client"
                    type="set"
                    to="test@test.com"
                    from="test@test.com"
                    id="uuid">
                  <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
                    <affiliations node="urn:xmpp:mds:displayed:0">
                      <affiliation jid="aim.shakespeare.lit"
                                   affiliation="member" />
                    </affiliations>
                  </pubsub>
                </iq>
              </privileged_iq>
            </iq>
            """,
            use_values=False,
        )
        self.recv(  # language=XML
            """
            <iq id="uuid"
                type="result"
                to="test@test.com"
                from="aim.shakespeare.lit">
              <privilege xmlns="urn:xmpp:privilege:2">
                <forwarded xmlns="urn:xmpp:forward:0">
                  <iq xmlns="jabber:client"
                      id="uuid"
                      type="result"
                      to="test@localhost" />
                </forwarded>
              </privilege>
            </iq>
            """
        )
        assert task.done()

    def test_receive_event(self):
        session = self.get_romeo_session()
        # juliet = self.juliet
        muc = self.get_private_muc()

        with unittest.mock.patch("test_muc.Session.on_displayed") as on_displayed:
            self.recv(  # language=XML
                f"""
            <message from='{session.user.jid}'
                     to='{self.xmpp.boundjid.bare}'
                     type='headline'
                     id='new-displayed-pep-event'>
              <event xmlns='http://jabber.org/protocol/pubsub#event'>
                <items node='urn:xmpp:mds:displayed:0'>
                  <item id='{muc.jid}'>
                    <displayed xmlns='urn:xmpp:mds:displayed:0'>
                      <stanza-id xmlns='urn:xmpp:sid:0'
                                 by='what@ev.er'
                                 id='1337' />
                    </displayed>
                  </item>
                </items>
              </event>
            </message>
            """
            )
            on_displayed.assert_awaited_once_with(muc, "legacy-1337")

    # class TestSendMDS(MDSMixin, BaseMUC):
    def test_send_mds(self):
        muc = self.get_private_muc()
        participant = self.run_coro(muc.get_user_participant())
        participant.displayed("legacy-msg-id")
        self.send(  # language=XML
            """
            <iq id="uuid"
                to="romeo@montague.lit"
                from="aim.shakespeare.lit">
              <privileged_iq xmlns="urn:xmpp:privilege:2">
                <iq xmlns="jabber:client"
                    to="romeo@montague.lit"
                    from="romeo@montague.lit"
                    id="uuid">
                  <pubsub xmlns="http://jabber.org/protocol/pubsub">
                    <publish node="urn:xmpp:mds:displayed:0">
                      <item id="room-private@aim.shakespeare.lit">
                        <displayed xmlns="urn:xmpp:mds:displayed:0">
                          <stanza-id xmlns="urn:xmpp:sid:0"
                                     id="msg-id"
                                     by="room-private@aim.shakespeare.lit" />
                        </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>
              </privileged_iq>
            </iq>
            """,
            use_values=False,
        )

M tests/test_session.py => tests/test_session.py +5 -1
@@ 12,7 12,7 @@ from slidge.util.types import LinkPreview


class Gateway(BaseGateway):
    pass
    COMPONENT_NAME = "A test"


class Session(BaseSession):


@@ 79,6 79,10 @@ class TestSession(AvatarFixtureMixin, SlidgeTest):
                      from="{self.xmpp.boundjid.bare}">
              <status>YUP</status>
              <show>chat</show>
              <c xmlns="http://jabber.org/protocol/caps"
                 node="http://slixmpp.com/ver/1.8.5"
                 hash="sha-1"
                 ver="MQesQ734HJbCNJOF87C/MoVNllM=" />
            </presence>
            """
        )

M tests/test_shakespeare.py => tests/test_shakespeare.py +2 -0
@@ 803,6 803,8 @@ class TestAimShakespeareBase(Base):
                <feature var="urn:xmpp:occupant-id:0" />
                <feature var="urn:xmpp:avatar:metadata+notify" />
                <feature var="urn:xmpp:hats:0" />
                <feature var="urn:xmpp:mds:displayed:0+notify" />
                <feature var="urn:xmpp:mds:displayed:0" />
              </query>
            </iq>
            """