plugin XEP-0050: small code quality improvements + add C.ENTITY_ADMINS magic key to allow administrators profiles
This commit is contained in:
parent
b8d00757d3
commit
f1cc34cc53
3 changed files with 146 additions and 82 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue