plugin XEP-0050: small code quality improvements + add C.ENTITY_ADMINS magic key to allow administrators profiles

This commit is contained in:
Goffi 2023-12-10 15:08:08 +01:00
commit f1cc34cc53
3 changed files with 146 additions and 82 deletions

View file

@ -92,7 +92,12 @@ class Const(object):
PROF_KEY_NONE = "@NONE@"
PROF_KEY_DEFAULT = "@DEFAULT@"
PROF_KEY_ALL = "@ALL@"
#: anybody
ENTITY_ALL = "@ALL@"
#: Bare jids of administrators
ENTITY_ADMINS = "@ADMIN@"
#: bare jid of client's profile
ENTITY_PROFILE_BARE = "@PROFILE_BAREJID@"
ENTITY_ALL_RESOURCES = "@ALL_RESOURCES@"
ENTITY_MAIN_RESOURCE = "@MAIN_RESOURCE@"
ENTITY_CAP_HASH = "CAP_HASH"

View file

@ -340,7 +340,7 @@ class AdHocDBus:
for found in found_data:
for device_jid_s in found:
device_jid = jid.JID(device_jid_s)
cmd_list = await self._c.list(client, device_jid)
cmd_list = await self._c.list_commands(client, device_jid)
for cmd in cmd_list:
if cmd.nodeIdentifier == NS_MEDIA_PLAYER:
try:

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# SàT plugin for Ad-Hoc Commands (XEP-0050)
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
# Libervia plugin for Ad-Hoc Commands (XEP-0050)
# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@ -19,11 +19,11 @@
from collections import namedtuple
from uuid import uuid4
from typing import List, Optional
from typing import Callable
from zope.interface import implementer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols import jabber
from twisted.words.protocols.jabber.error import StanzaError
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from twisted.internet import defer
@ -31,7 +31,7 @@ from wokkel import disco, iwokkel, data_form
from libervia.backend.core.i18n import _, D_
from libervia.backend.core.constants import Const as C
from libervia.backend.core.log import getLogger
from libervia.backend.core.xmpp import SatXMPPEntity
from libervia.backend.core.xmpp import SatXMPPClient, SatXMPPEntity
from libervia.backend.core import exceptions
from libervia.backend.memory.memory import Sessions
from libervia.backend.tools import xml_tools, utils
@ -68,9 +68,29 @@ PLUGIN_INFO = {
}
Status = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))
Action = namedtuple("Action", ("EXECUTE", "CANCEL", "NEXT", "PREV"))
Note = namedtuple("Note", ("INFO", "WARN", "ERROR"))
Error = namedtuple(
"Error",
(
"MALFORMED_ACTION",
"BAD_ACTION",
"BAD_LOCALE",
"BAD_PAYLOAD",
"BAD_SESSIONID",
"SESSION_EXPIRED",
"FORBIDDEN",
"ITEM_NOT_FOUND",
"FEATURE_NOT_IMPLEMENTED",
"INTERNAL",
),
)
class AdHocError(Exception):
def __init__(self, error_const):
""" Error to be used from callback
"""Error to be used from callback
@param error_const: one of XEP_0050.ERROR
"""
assert error_const in XEP_0050.ERROR
@ -79,10 +99,19 @@ class AdHocError(Exception):
@implementer(iwokkel.IDisco)
class AdHocCommand(XMPPHandler):
def __init__(self, callback, label, node, features, timeout,
allowed_jids, allowed_groups, allowed_magics, forbidden_jids,
forbidden_groups):
def __init__(
self,
callback,
label,
node,
features,
timeout,
allowed_jids,
allowed_groups,
allowed_magics,
forbidden_jids,
forbidden_groups,
):
XMPPHandler.__init__(self)
self.callback = callback
self.label = label
@ -103,7 +132,7 @@ class AdHocCommand(XMPPHandler):
return self.label
def is_authorised(self, requestor):
if "@ALL@" in self.allowed_magics:
if C.ENTITY_ALL in self.allowed_magics:
return True
forbidden = set(self.forbidden_jids)
for group in self.forbidden_groups:
@ -115,8 +144,11 @@ class AdHocCommand(XMPPHandler):
try:
allowed.update(self.client.roster.get_jids_from_group(group))
except exceptions.UnknownGroupError:
log.warning(_("The groups [{group}] is unknown for profile [{profile}])")
.format(group=group, profile=self.client.profile))
log.warning(
_("The groups [{group}] is unknown for profile [{profile}])").format(
group=group, profile=self.client.profile
)
)
if requestor.userhostJID() in allowed:
return True
return False
@ -132,17 +164,26 @@ class AdHocCommand(XMPPHandler):
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
return []
def _sendAnswer(self, callback_data, session_id, request):
""" Send result of the command
def _sendAnswer(
self,
callback_data: tuple[
tuple[domish.Element, None],
Status,
list[str] | None,
tuple[Note, str] | None,
],
session_id,
request,
) -> None:
"""Send result of the command
@param callback_data: tuple (payload, status, actions, note) with:
- payload (domish.Element, None) usualy containing data form
- status: current status, see XEP_0050.STATUS
- actions(list[str], None): list of allowed actions (see XEP_0050.ACTION).
First action is the default one. Default to EXECUTE
- note(tuple[str, unicode]): optional additional note: either None or a
tuple with (note type, human readable string), "note type" being in
XEP_0050.NOTE
- payload: usualy containing data form
- status: current status
- actions: list of allowed actions (see XEP_0050.ACTION).
First action is the default one. Default to EXECUTE
- note: optional additional note: either None or a tuple with (note type,
human readable string)
@param session_id: current session id
@param request: original request (domish.Element)
@return: deferred
@ -181,13 +222,13 @@ class AdHocCommand(XMPPHandler):
del self.sessions[session_id]
def _sendError(self, error_constant, session_id, request):
""" Send error stanza
"""Send error stanza
@param error_constant: one of XEP_OO50.ERROR
@param request: original request (domish.Element)
"""
xmpp_condition, cmd_condition = error_constant
iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request)
iq_elt = StanzaError(xmpp_condition).toResponse(request)
if cmd_condition:
error_elt = next(iq_elt.elements(None, "error"))
error_elt.addElement(cmd_condition, NS_COMMANDS)
@ -237,29 +278,11 @@ class AdHocCommand(XMPPHandler):
d.addErrback(self._request_eb, command_elt.parent, session_id)
class XEP_0050(object):
STATUS = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))(
"executing", "completed", "canceled"
)
ACTION = namedtuple("Action", ("EXECUTE", "CANCEL", "NEXT", "PREV"))(
"execute", "cancel", "next", "prev"
)
NOTE = namedtuple("Note", ("INFO", "WARN", "ERROR"))("info", "warn", "error")
ERROR = namedtuple(
"Error",
(
"MALFORMED_ACTION",
"BAD_ACTION",
"BAD_LOCALE",
"BAD_PAYLOAD",
"BAD_SESSIONID",
"SESSION_EXPIRED",
"FORBIDDEN",
"ITEM_NOT_FOUND",
"FEATURE_NOT_IMPLEMENTED",
"INTERNAL",
),
)(
class XEP_0050:
STATUS = Status("executing", "completed", "canceled")
ACTION = Action("execute", "cancel", "next", "prev")
NOTE = Note("info", "warn", "error")
ERROR = Error(
("bad-request", "malformed-action"),
("bad-request", "bad-action"),
("bad-request", "bad-locale"),
@ -309,7 +332,7 @@ class XEP_0050(object):
security_limit=2,
help_string=D_("Execute ad-hoc commands"),
)
host.register_namespace('commands', NS_COMMANDS)
host.register_namespace("commands", NS_COMMANDS)
def get_handler(self, client):
return XEP_0050_handler(self)
@ -320,8 +343,16 @@ class XEP_0050(object):
if not client.is_component:
self.add_ad_hoc_command(client, self._status_callback, _("Status"))
def do(self, client, entity, node, action=ACTION.EXECUTE, session_id=None,
form_values=None, timeout=30):
def do(
self,
client,
entity,
node,
action=ACTION.EXECUTE,
session_id=None,
form_values=None,
timeout=30,
):
"""Do an Ad-Hoc Command
@param entity(jid.JID): entity which will execture the command
@ -363,7 +394,7 @@ class XEP_0050(object):
raise AdHocError(error_type)
def _items_2_xmlui(self, items, no_instructions):
"""Convert discovery items to XMLUI dialog """
"""Convert discovery items to XMLUI dialog"""
# TODO: manage items on different jids
form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id)
@ -418,7 +449,6 @@ class XEP_0050(object):
return command_elt, data
def _commands_answer_2_xmlui(self, iq_elt, session_id, session_data):
"""Convert command answer to an ui for frontend
@ -486,7 +516,9 @@ class XEP_0050(object):
form = data_form.Form.fromElement(data_elt)
# we add any present note to the instructions
form.instructions.extend(self._merge_notes(notes))
return xml_tools.data_form_2_xmlui(form, self.__requesting_id, session_id=session_id)
return xml_tools.data_form_2_xmlui(
form, self.__requesting_id, session_id=session_id
)
def _requesting_entity(self, data, profile):
def serialise(ret_data):
@ -550,8 +582,14 @@ class XEP_0050(object):
remote_id = session_data.get("remote_id")
# we request execute node's command
d = self.do(client, entity, session_data["node"], action=XEP_0050.ACTION.EXECUTE,
session_id=remote_id, form_values=data_form_values)
d = self.do(
client,
entity,
session_data["node"],
action=XEP_0050.ACTION.EXECUTE,
session_id=remote_id,
form_values=data_form_values,
)
d.addCallback(self._commands_answer_2_xmlui, session_id, session_data)
d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {})
@ -645,7 +683,7 @@ class XEP_0050(object):
xmlui.session_id = session_id
return xmlui
def list(self, client, to_jid):
def list_commands(self, client, to_jid):
"""Request available commands
@param to_jid(jid.JID, None): the entity answering the commands
@ -670,7 +708,7 @@ class XEP_0050(object):
@param no_instructions(bool): if True, don't add instructions widget
@return D(xml_tools.XMLUI): UI with the commands
"""
d = self.list(client, to_jid)
d = self.list_commands(client, to_jid)
d.addCallback(self._items_2_xmlui, no_instructions)
return d
@ -685,9 +723,9 @@ class XEP_0050(object):
async def sequence(
self,
client: SatXMPPEntity,
sequence: List[dict],
sequence: list[dict],
node: str,
service_jid: Optional[jid.JID] = None,
service_jid: jid.JID | None = None,
) -> dict:
"""Send a series of data to an ad-hoc service
@ -698,6 +736,8 @@ class XEP_0050(object):
None to use profile's server
@return: data received in final answer
"""
assert sequence
answer_data = None
if service_jid is None:
service_jid = jid.JID(client.jid.host)
@ -714,73 +754,93 @@ class XEP_0050(object):
__, answer_data = self.parse_command_answer(iq_result_elt)
session_id = answer_data.pop("session_id")
assert answer_data is not None
return answer_data
def add_ad_hoc_command(self, client, callback, label, node=None, features=None,
timeout=600, allowed_jids=None, allowed_groups=None,
allowed_magics=None, forbidden_jids=None, forbidden_groups=None,
):
def add_ad_hoc_command(
self,
client: SatXMPPClient,
callback: Callable,
label: str,
node: str | None = None,
features: list[str] | None = None,
timeout: int = 600,
allowed_jids: list[jid.JID] | None = None,
allowed_groups: list[str] | None = None,
allowed_magics: list[str] | None = None,
forbidden_jids: list[jid.JID] | None = None,
forbidden_groups: list[str] | None = None,
) -> str:
"""Add an ad-hoc command for the current profile
@param callback: method associated with this ad-hoc command which return the
payload data (see AdHocCommand._sendAnswer), can return a
deferred
payload data (see AdHocCommand._sendAnswer), can return a deferred
@param label: label associated with this command on the main menu
@param node: disco item node associated with this command. None to use
autogenerated node
autogenerated node
@param features: features associated with the payload (list of strings), usualy
data form
data form
@param timeout: delay between two requests before canceling the session (in
seconds)
seconds)
@param allowed_jids: list of allowed entities
@param allowed_groups: list of allowed roster groups
@param allowed_magics: list of allowed magic keys, can be:
@ALL@: allow everybody
@PROFILE_BAREJID@: allow only the jid of the profile
C.ENTITY_ALL: allow everybody
C.ENTITY_PROFILE_BARE: allow only the jid of the profile
C.ENTITY_ADMINS: any administrator user
@param forbidden_jids: black list of entities which can't access this command
@param forbidden_groups: black list of groups which can't access this command
@return: node of the added command, useful to remove the command later
"""
# FIXME: "@ALL@" for profile_key seems useless and dangerous
commands = client._XEP_0050_commands
if node is None:
node = "%s_%s" % ("COMMANDS", uuid4())
node = label.lower().replace(" ", "_")
if not node:
node = f"COMMANDS_{uuid4()}"
elif node in commands:
node = f"{node}_{uuid4()}"
if features is None:
features = [data_form.NS_X_DATA]
if allowed_jids is None:
allowed_jids = []
else:
# we don't want to modify the initial list
allowed_jids = allowed_jids.copy()
if allowed_groups is None:
allowed_groups = []
if allowed_magics is None:
allowed_magics = ["@PROFILE_BAREJID@"]
allowed_magics = [C.ENTITY_PROFILE_BARE]
if forbidden_jids is None:
forbidden_jids = []
if forbidden_groups is None:
forbidden_groups = []
# TODO: manage newly created/removed profiles
_allowed_jids = (
(allowed_jids + [client.jid.userhostJID()])
if "@PROFILE_BAREJID@" in allowed_magics
else allowed_jids
)
if C.ENTITY_PROFILE_BARE in allowed_magics:
allowed_jids += [client.jid.userhostJID()]
# TODO: manage dynamic addition/removal of admin status once possible
if C.ENTITY_ADMINS in allowed_magics:
allowed_jids += list(self.host.memory.admin_jids)
ad_hoc_command = AdHocCommand(
callback,
label,
node,
features,
timeout,
_allowed_jids,
allowed_jids,
allowed_groups,
allowed_magics,
forbidden_jids,
forbidden_groups,
)
ad_hoc_command.setHandlerParent(client)
commands = client._XEP_0050_commands
commands[node] = ad_hoc_command
return node
def on_cmd_request(self, request, client):
request.handled = True
@ -803,7 +863,6 @@ class XEP_0050(object):
@implementer(iwokkel.IDisco)
class XEP_0050_handler(XMPPHandler):
def __init__(self, plugin_parent):
self.plugin_parent = plugin_parent