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:
parent
ea5e0a46f5
commit
3b5d2313a2
1 changed files with 232 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue