Mercurial > libervia-backend
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 |