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.
This commit is contained in:
Goffi 2020-08-01 16:12:44 +02:00
commit 3b5d2313a2

View file

@ -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 @@ PLUGIN_INFO = {
}
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 @@ SHARE_TYPES = (TYPE_PATH, TYPE_VIRTUAL)
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 @@ class XEP_0329(object):
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 @@ class XEP_0329(object):
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 @@ class XEP_0329_handler(xmlstream.XMPPHandler):
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