comparison sat/plugins/plugin_xep_0329.py @ 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 5887fb414758
children 8bbd2ed924e8
comparison
equal deleted inserted replaced
3319:3a15e76a694e 3320:bb92085720c8
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from sat.core.i18n import _ 19 import mimetypes
20 from sat.core import exceptions 20 import json
21 from sat.core.constants import Const as C 21 import os
22 from sat.core.log import getLogger 22 from pathlib import Path
23 23 from typing import Optional, Dict
24 log = getLogger(__name__)
25 from sat.tools import stream
26 from sat.tools.common import regex
27 from wokkel import disco, iwokkel
28 from zope.interface import implementer 24 from zope.interface import implementer
29 from twisted.words.protocols.jabber import xmlstream 25 from twisted.words.protocols.jabber import xmlstream
30 from twisted.words.protocols.jabber import jid 26 from twisted.words.protocols.jabber import jid
31 from twisted.words.protocols.jabber import error as jabber_error 27 from twisted.words.protocols.jabber import error as jabber_error
32 from twisted.internet import defer 28 from twisted.internet import defer
33 import mimetypes 29 from wokkel import disco, iwokkel, data_form
34 import json 30 from sat.core.i18n import _
35 import os 31 from sat.core.xmpp import SatXMPPEntity
36 32 from sat.core import exceptions
33 from sat.core.constants import Const as C
34 from sat.core.log import getLogger
35 from sat.tools import stream
36 from sat.tools.common import regex
37
38
39 log = getLogger(__name__)
37 40
38 PLUGIN_INFO = { 41 PLUGIN_INFO = {
39 C.PI_NAME: "File Information Sharing", 42 C.PI_NAME: "File Information Sharing",
40 C.PI_IMPORT_NAME: "XEP-0329", 43 C.PI_IMPORT_NAME: "XEP-0329",
41 C.PI_TYPE: "XEP", 44 C.PI_TYPE: "XEP",
46 C.PI_HANDLER: "yes", 49 C.PI_HANDLER: "yes",
47 C.PI_DESCRIPTION: _("""Implementation of File Information Sharing"""), 50 C.PI_DESCRIPTION: _("""Implementation of File Information Sharing"""),
48 } 51 }
49 52
50 NS_FIS = "urn:xmpp:fis:0" 53 NS_FIS = "urn:xmpp:fis:0"
51 54 NS_FIS_AFFILIATION = "org.salut-a-toi.fis-affiliation"
52 IQ_FIS_REQUEST = C.IQ_GET + '/query[@xmlns="' + NS_FIS + '"]' 55 NS_FIS_CONFIGURATION = "org.salut-a-toi.fis-configuration"
56
57 IQ_FIS_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_FIS}"]'
58 # not in the standard, but needed, and it's handy to keep it here
59 IQ_FIS_AFFILIATION_GET = f'{C.IQ_GET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]'
60 IQ_FIS_AFFILIATION_SET = f'{C.IQ_SET}/affiliations[@xmlns="{NS_FIS_AFFILIATION}"]'
53 SINGLE_FILES_DIR = "files" 61 SINGLE_FILES_DIR = "files"
54 TYPE_VIRTUAL = "virtual" 62 TYPE_VIRTUAL = "virtual"
55 TYPE_PATH = "path" 63 TYPE_PATH = "path"
56 SHARE_TYPES = (TYPE_PATH, TYPE_VIRTUAL) 64 SHARE_TYPES = (TYPE_PATH, TYPE_VIRTUAL)
57 KEY_TYPE = "type" 65 KEY_TYPE = "type"
66
67
68 class RootPathException(Exception):
69 """Root path is requested"""
58 70
59 71
60 class ShareNode(object): 72 class ShareNode(object):
61 """Node containing directory or files to share, virtual or real""" 73 """Node containing directory or files to share, virtual or real"""
62 74
286 "FISUnsharePath", 298 "FISUnsharePath",
287 ".plugin", 299 ".plugin",
288 in_sign="ss", 300 in_sign="ss",
289 out_sign="", 301 out_sign="",
290 method=self._unsharePath, 302 method=self._unsharePath,
303 )
304 host.bridge.addMethod(
305 "FISAffiliationsGet",
306 ".plugin",
307 in_sign="ssss",
308 out_sign="a{ss}",
309 method=self._affiliationsGet,
310 async_=True,
311 )
312 host.bridge.addMethod(
313 "FISAffiliationsSet",
314 ".plugin",
315 in_sign="sssa{ss}s",
316 out_sign="",
317 method=self._affiliationsSet,
318 async_=True,
291 ) 319 )
292 host.bridge.addSignal("FISSharedPathNew", ".plugin", signature="sss") 320 host.bridge.addSignal("FISSharedPathNew", ".plugin", signature="sss")
293 host.bridge.addSignal("FISSharedPathRemoved", ".plugin", signature="ss") 321 host.bridge.addSignal("FISSharedPathRemoved", ".plugin", signature="ss")
294 host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger) 322 host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger)
295 host.registerNamespace("fis", NS_FIS) 323 host.registerNamespace("fis", NS_FIS)
654 ) 682 )
655 continue 683 continue
656 files.append(file_data) 684 files.append(file_data)
657 return files 685 return files
658 686
687 # affiliations #
688
689 async def _parseElement(self, client, iq_elt, element, namespace):
690 from_jid = jid.JID(iq_elt['from'])
691 elt = next(iq_elt.elements(namespace, element))
692 path = Path("/", elt['path'])
693 if len(path.parts) < 2:
694 raise RootPathException
695 namespace = elt.getAttribute('namespace')
696 files_data = await self.host.memory.getFiles(
697 client,
698 peer_jid=from_jid,
699 path=str(path.parent),
700 name=path.name,
701 namespace=namespace,
702 )
703 if len(files_data) != 1:
704 client.sendError(iq_elt, 'item-not-found')
705 raise exceptions.CancelError
706 file_data = files_data[0]
707 return from_jid, elt, path, namespace, file_data
708
709 def _affiliationsGet(self, service_jid_s, namespace, path, profile):
710 client = self.host.getClient(profile)
711 service = jid.JID(service_jid_s)
712 d = defer.ensureDeferred(self.affiliationsGet(
713 client, service, namespace or None, path))
714 d.addCallback(
715 lambda affiliations: {
716 str(entity): affiliation for entity, affiliation in affiliations.items()
717 }
718 )
719 return d
720
721 async def affiliationsGet(
722 self,
723 client: SatXMPPEntity,
724 service: jid.JID,
725 namespace: Optional[str],
726 path: str
727 ) -> Dict[jid.JID, str]:
728 if not path:
729 raise ValueError(f"invalid path: {path!r}")
730 iq_elt = client.IQ("get")
731 iq_elt['to'] = service.full()
732 affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations"))
733 if namespace:
734 affiliations_elt["namespace"] = namespace
735 affiliations_elt["path"] = path
736 iq_result_elt = await iq_elt.send()
737 try:
738 affiliations_elt = next(iq_result_elt.elements(NS_FIS_AFFILIATION, "affiliations"))
739 except StopIteration:
740 raise exceptions.DataError(f"Invalid result to affiliations request: {iq_result_elt.toXml()}")
741
742 affiliations = {}
743 for affiliation_elt in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation'):
744 try:
745 affiliations[jid.JID(affiliation_elt['jid'])] = affiliation_elt['affiliation']
746 except (KeyError, RuntimeError):
747 raise exceptions.DataError(
748 f"invalid affiliation element: {affiliation_elt.toXml()}")
749
750 return affiliations
751
752 def _affiliationsSet(self, service_jid_s, namespace, path, affiliations, profile):
753 client = self.host.getClient(profile)
754 service = jid.JID(service_jid_s)
755 affiliations = {jid.JID(e): a for e, a in affiliations.items()}
756 return defer.ensureDeferred(self.affiliationsSet(
757 client, service, namespace or None, path, affiliations))
758
759 async def affiliationsSet(
760 self,
761 client: SatXMPPEntity,
762 service: jid.JID,
763 namespace: Optional[str],
764 path: str,
765 affiliations: Dict[jid.JID, str],
766 ):
767 if not path:
768 raise ValueError(f"invalid path: {path!r}")
769 iq_elt = client.IQ("set")
770 iq_elt['to'] = service.full()
771 affiliations_elt = iq_elt.addElement((NS_FIS_AFFILIATION, "affiliations"))
772 if namespace:
773 affiliations_elt["namespace"] = namespace
774 affiliations_elt["path"] = path
775 for entity_jid, affiliation in affiliations.items():
776 affiliation_elt = affiliations_elt.addElement('affiliation')
777 affiliation_elt['jid'] = entity_jid.full()
778 affiliation_elt['affiliation'] = affiliation
779 await iq_elt.send()
780
781 def _onComponentAffiliationsGet(self, iq_elt, client):
782 iq_elt.handled = True
783 defer.ensureDeferred(self.onComponentAffiliationsGet(client, iq_elt))
784
785 async def onComponentAffiliationsGet(self, client, iq_elt):
786 try:
787 (
788 from_jid, affiliations_elt, path, namespace, file_data
789 ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION)
790 except exceptions.CancelError:
791 return
792 except RootPathException:
793 # if root path is requested, we only get owner affiliation
794 peer_jid, owner = self._compParseJids(client, iq_elt)
795 is_owner = peer_jid.userhostJID() == owner
796 affiliations = {owner: 'owner'}
797 else:
798 from_jid_bare = from_jid.userhostJID()
799 is_owner = from_jid_bare == file_data.get('owner')
800 affiliations = self.host.memory.getFileAffiliations(file_data)
801 iq_result_elt = xmlstream.toResponse(iq_elt, "result")
802 affiliations_elt = iq_result_elt.addElement((NS_FIS_AFFILIATION, 'affiliations'))
803 for entity_jid, affiliation in affiliations.items():
804 if not is_owner and entity_jid.userhostJID() != from_jid_bare:
805 # only onwer can get all affiliations
806 continue
807 affiliation_elt = affiliations_elt.addElement('affiliation')
808 affiliation_elt['jid'] = entity_jid.userhost()
809 affiliation_elt['affiliation'] = affiliation
810 client.send(iq_result_elt)
811
812 def _onComponentAffiliationsSet(self, iq_elt, client):
813 iq_elt.handled = True
814 defer.ensureDeferred(self.onComponentAffiliationsSet(client, iq_elt))
815
816 async def onComponentAffiliationsSet(self, client, iq_elt):
817 try:
818 (
819 from_jid, affiliations_elt, path, namespace, file_data
820 ) = await self._parseElement(client, iq_elt, "affiliations", NS_FIS_AFFILIATION)
821 except exceptions.CancelError:
822 return
823 except RootPathException:
824 client.sendError(iq_elt, 'bad-request', "Root path can't be used")
825 return
826
827 if from_jid.userhostJID() != file_data['owner']:
828 log.warning(
829 f"{from_jid} tried to modify {path} affiliations while the owner is "
830 f"{file_data['owner']}"
831 )
832 client.sendError(iq_elt, 'forbidden')
833 return
834
835 try:
836 affiliations = {
837 jid.JID(e['jid']): e['affiliation']
838 for e in affiliations_elt.elements(NS_FIS_AFFILIATION, 'affiliation')
839 }
840 except (KeyError, RuntimeError):
841 log.warning(
842 f"invalid affiliation element: {affiliations_elt.toXml()}"
843 )
844 client.sendError(iq_elt, 'bad-request', "invalid affiliation element")
845 return
846 except Exception as e:
847 log.error(
848 f"unexepected exception while setting affiliation element: {e}\n"
849 f"{affiliations_elt.toXml()}"
850 )
851 client.sendError(iq_elt, 'internal-server-error', f"{e}")
852 return
853
854 await self.host.memory.setFileAffiliations(client, file_data, affiliations)
855
856 iq_result_elt = xmlstream.toResponse(iq_elt, "result")
857 client.send(iq_result_elt)
858
659 # file methods # 859 # file methods #
660 860
661 def _serializeData(self, files_data): 861 def _serializeData(self, files_data):
662 for file_data in files_data: 862 for file_data in files_data:
663 for key, value in file_data.items(): 863 for key, value in file_data.items():
761 def connectionInitialized(self): 961 def connectionInitialized(self):
762 if self.parent.is_component: 962 if self.parent.is_component:
763 self.xmlstream.addObserver( 963 self.xmlstream.addObserver(
764 IQ_FIS_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent 964 IQ_FIS_REQUEST, self.plugin_parent.onComponentRequest, client=self.parent
765 ) 965 )
966 self.xmlstream.addObserver(
967 IQ_FIS_AFFILIATION_GET,
968 self.plugin_parent._onComponentAffiliationsGet,
969 client=self.parent
970 )
971 self.xmlstream.addObserver(
972 IQ_FIS_AFFILIATION_SET,
973 self.plugin_parent._onComponentAffiliationsSet,
974 client=self.parent
975 )
976 self.xmlstream.addObserver(
977 IQ_FIS_CONFIGURATION_GET,
978 self.plugin_parent._onComponentConfigurationGet,
979 client=self.parent
980 )
981 self.xmlstream.addObserver(
982 IQ_FIS_CONFIGURATION_SET,
983 self.plugin_parent._onComponentConfigurationSet,
984 client=self.parent
985 )
766 else: 986 else:
767 self.xmlstream.addObserver( 987 self.xmlstream.addObserver(
768 IQ_FIS_REQUEST, self.plugin_parent.onRequest, client=self.parent 988 IQ_FIS_REQUEST, self.plugin_parent.onRequest, client=self.parent
769 ) 989 )
770 990