changeset 3320:bb92085720c8

plugin XEP-0329: implemented ways to get/set affiliations: PubSub-like affiliations can now be get/set using non standard methods. This has been done to prepare the move to a pubsub based file sharing system.
author Goffi <goffi@goffi.org>
date Sat, 01 Aug 2020 16:12:44 +0200
parents 3a15e76a694e
children 8bbd2ed924e8
files sat/plugins/plugin_xep_0329.py
diffstat 1 files changed, 233 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_xep_0329.py	Sat Aug 01 16:07:39 2020 +0200
+++ b/sat/plugins/plugin_xep_0329.py	Sat Aug 01 16:12:44 2020 +0200
@@ -16,25 +16,28 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from sat.core.i18n import _
-from sat.core import exceptions
-from sat.core.constants import Const as C
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-from sat.tools import stream
-from sat.tools.common import regex
-from wokkel import disco, iwokkel
+import mimetypes
+import json
+import os
+from pathlib import Path
+from typing import Optional, Dict
 from zope.interface import implementer
 from twisted.words.protocols.jabber import xmlstream
 from twisted.words.protocols.jabber import jid
 from twisted.words.protocols.jabber import error as jabber_error
 from twisted.internet import defer
-import mimetypes
-import json
-import os
+from wokkel import disco, iwokkel, data_form
+from sat.core.i18n import _
+from sat.core.xmpp import SatXMPPEntity
+from sat.core import exceptions
+from sat.core.constants import Const as C
+from sat.core.log import getLogger
+from sat.tools import stream
+from sat.tools.common import regex
 
 
+log = getLogger(__name__)
+
 PLUGIN_INFO = {
     C.PI_NAME: "File Information Sharing",
     C.PI_IMPORT_NAME: "XEP-0329",
@@ -48,8 +51,13 @@
 }
 
 NS_FIS = "urn:xmpp:fis:0"
+NS_FIS_AFFILIATION = "org.salut-a-toi.fis-affiliation"
+NS_FIS_CONFIGURATION = "org.salut-a-toi.fis-configuration"
 
-IQ_FIS_REQUEST = C.IQ_GET + '/query[@xmlns="' + NS_FIS + '"]'
+IQ_FIS_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_FIS}"]'
+# not in the standard, but needed, and it's handy to keep it here
+IQ_FIS_AFFILIATION_GET = f'{C.IQ_GET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]'
+IQ_FIS_AFFILIATION_SET = f'{C.IQ_SET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]'
 SINGLE_FILES_DIR = "files"
 TYPE_VIRTUAL = "virtual"
 TYPE_PATH = "path"
@@ -57,6 +65,10 @@
 KEY_TYPE = "type"
 
 
+class RootPathException(Exception):
+    """Root path is requested"""
+
+
 class ShareNode(object):
     """Node containing directory or files to share, virtual or real"""
 
@@ -289,6 +301,22 @@
             out_sign="",
             method=self._unsharePath,
         )
+        host.bridge.addMethod(
+            "FISAffiliationsGet",
+            ".plugin",
+            in_sign="ssss",
+            out_sign="a{ss}",
+            method=self._affiliationsGet,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "FISAffiliationsSet",
+            ".plugin",
+            in_sign="sssa{ss}s",
+            out_sign="",
+            method=self._affiliationsSet,
+            async_=True,
+        )
         host.bridge.addSignal("FISSharedPathNew", ".plugin", signature="sss")
         host.bridge.addSignal("FISSharedPathRemoved", ".plugin", signature="ss")
         host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger)
@@ -656,6 +684,178 @@
             files.append(file_data)
         return files
 
+    # affiliations #
+
+    async def _parseElement(self, client, iq_elt, element, namespace):
+        from_jid = jid.JID(iq_elt['from'])
+        elt = next(iq_elt.elements(namespace, element))
+        path = Path("/", elt['path'])
+        if len(path.parts) < 2:
+            raise RootPathException
+        namespace = elt.getAttribute('namespace')
+        files_data = await self.host.memory.getFiles(
+            client,
+            peer_jid=from_jid,
+            path=str(path.parent),
+            name=path.name,
+            namespace=namespace,
+        )
+        if len(files_data) != 1:
+            client.sendError(iq_elt, 'item-not-found')
+            raise exceptions.CancelError
+        file_data = files_data[0]
+        return from_jid, elt, path, namespace, file_data
+
+    def _affiliationsGet(self, service_jid_s, namespace, path, profile):
+        client = self.host.getClient(profile)
+        service = jid.JID(service_jid_s)
+        d = defer.ensureDeferred(self.affiliationsGet(
+            client, service, namespace or None, path))
+        d.addCallback(
+            lambda affiliations: {
+                str(entity): affiliation for entity, affiliation in affiliations.items()
+            }
+        )
+        return d
+
+    async def affiliationsGet(
+        self,
+        client: SatXMPPEntity,
+        service: jid.JID,
+        namespace: Optional[str],
+        path: str
+    ) -> Dict[jid.JID, str]:
+        if not path:
+            raise ValueError(f"invalid path: {path!r}")
+        iq_elt = client.IQ("get")
+        iq_elt['to'] = service.full()
+        affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations"))
+        if namespace:
+            affiliations_elt["namespace"] = namespace
+        affiliations_elt["path"] = path
+        iq_result_elt = await iq_elt.send()
+        try:
+            affiliations_elt = next(iq_result_elt.elements(NS_FIS_AFFILIATION, "affiliations"))
+        except StopIteration:
+            raise exceptions.DataError(f"Invalid result to affiliations request: {iq_result_elt.toXml()}")
+
+        affiliations = {}
+        for affiliation_elt in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation'):
+            try:
+                affiliations[jid.JID(affiliation_elt['jid'])] = affiliation_elt['affiliation']
+            except (KeyError, RuntimeError):
+                raise exceptions.DataError(
+                    f"invalid affiliation element: {affiliation_elt.toXml()}")
+
+        return affiliations
+
+    def _affiliationsSet(self, service_jid_s, namespace, path, affiliations, profile):
+        client = self.host.getClient(profile)
+        service = jid.JID(service_jid_s)
+        affiliations = {jid.JID(e): a for e, a in affiliations.items()}
+        return defer.ensureDeferred(self.affiliationsSet(
+            client, service, namespace or None, path, affiliations))
+
+    async def affiliationsSet(
+        self,
+        client: SatXMPPEntity,
+        service: jid.JID,
+        namespace: Optional[str],
+        path: str,
+        affiliations: Dict[jid.JID, str],
+    ):
+        if not path:
+            raise ValueError(f"invalid path: {path!r}")
+        iq_elt = client.IQ("set")
+        iq_elt['to'] = service.full()
+        affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations"))
+        if namespace:
+            affiliations_elt["namespace"] = namespace
+        affiliations_elt["path"] = path
+        for entity_jid, affiliation in affiliations.items():
+            affiliation_elt = affiliations_elt.addElement('affiliation')
+            affiliation_elt['jid'] = entity_jid.full()
+            affiliation_elt['affiliation'] = affiliation
+        await iq_elt.send()
+
+    def _onComponentAffiliationsGet(self, iq_elt, client):
+        iq_elt.handled = True
+        defer.ensureDeferred(self.onComponentAffiliationsGet(client, iq_elt))
+
+    async def onComponentAffiliationsGet(self, client, iq_elt):
+        try:
+            (
+                from_jid, affiliations_elt, path, namespace, file_data
+            ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION)
+        except exceptions.CancelError:
+            return
+        except RootPathException:
+            # if root path is requested, we only get owner affiliation
+            peer_jid, owner = self._compParseJids(client, iq_elt)
+            is_owner = peer_jid.userhostJID() == owner
+            affiliations = {owner: 'owner'}
+        else:
+            from_jid_bare = from_jid.userhostJID()
+            is_owner = from_jid_bare == file_data.get('owner')
+            affiliations = self.host.memory.getFileAffiliations(file_data)
+        iq_result_elt = xmlstream.toResponse(iq_elt, "result")
+        affiliations_elt = iq_result_elt.addElement((NS_FIS_AFFILIATION, 'affiliations'))
+        for entity_jid, affiliation in affiliations.items():
+            if not is_owner and entity_jid.userhostJID() != from_jid_bare:
+                # only onwer can get all affiliations
+                continue
+            affiliation_elt = affiliations_elt.addElement('affiliation')
+            affiliation_elt['jid'] = entity_jid.userhost()
+            affiliation_elt['affiliation'] = affiliation
+        client.send(iq_result_elt)
+
+    def _onComponentAffiliationsSet(self, iq_elt, client):
+        iq_elt.handled = True
+        defer.ensureDeferred(self.onComponentAffiliationsSet(client, iq_elt))
+
+    async def onComponentAffiliationsSet(self, client, iq_elt):
+        try:
+            (
+                from_jid, affiliations_elt, path, namespace, file_data
+            ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION)
+        except exceptions.CancelError:
+            return
+        except RootPathException:
+            client.sendError(iq_elt, 'bad-request', "Root path can't be used")
+            return
+
+        if from_jid.userhostJID() != file_data['owner']:
+            log.warning(
+                f"{from_jid} tried to modify {path} affiliations while the owner is "
+                f"{file_data['owner']}"
+            )
+            client.sendError(iq_elt, 'forbidden')
+            return
+
+        try:
+            affiliations = {
+                jid.JID(e['jid']): e['affiliation']
+                for e in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation')
+            }
+        except (KeyError, RuntimeError):
+                log.warning(
+                    f"invalid affiliation element: {affiliations_elt.toXml()}"
+                )
+                client.sendError(iq_elt, 'bad-request', "invalid affiliation element")
+                return
+        except Exception as e:
+                log.error(
+                    f"unexepected exception while setting  affiliation element: {e}\n"
+                    f"{affiliations_elt.toXml()}"
+                )
+                client.sendError(iq_elt, 'internal-server-error', f"{e}")
+                return
+
+        await self.host.memory.setFileAffiliations(client, file_data, affiliations)
+
+        iq_result_elt = xmlstream.toResponse(iq_elt, "result")
+        client.send(iq_result_elt)
+
     # file methods #
 
     def _serializeData(self, files_data):
@@ -763,6 +963,26 @@
             self.xmlstream.addObserver(
                 IQ_FIS_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent
             )
+            self.xmlstream.addObserver(
+                IQ_FIS_AFFILIATION_GET,
+                self.plugin_parent._onComponentAffiliationsGet,
+                client=self.parent
+            )
+            self.xmlstream.addObserver(
+                IQ_FIS_AFFILIATION_SET,
+                self.plugin_parent._onComponentAffiliationsSet,
+                client=self.parent
+            )
+            self.xmlstream.addObserver(
+                IQ_FIS_CONFIGURATION_GET,
+                self.plugin_parent._onComponentConfigurationGet,
+                client=self.parent
+            )
+            self.xmlstream.addObserver(
+                IQ_FIS_CONFIGURATION_SET,
+                self.plugin_parent._onComponentConfigurationSet,
+                client=self.parent
+            )
         else:
             self.xmlstream.addObserver(
                 IQ_FIS_REQUEST, self.plugin_parent.onRequest, client=self.parent