# HG changeset patch # User Goffi # Date 1702217288 -3600 # Node ID 50c919dfe61b13e5f275c9a109edce9f55b8a61c # Parent 6784d07b99c89f3bcdbf847dd7ced52b8bffc8da plugin XEP-0050: small code quality improvements + add `C.ENTITY_ADMINS` magic key to allow administrators profiles diff -r 6784d07b99c8 -r 50c919dfe61b libervia/backend/core/constants.py --- a/libervia/backend/core/constants.py Sat Dec 09 19:20:13 2023 +0100 +++ b/libervia/backend/core/constants.py Sun Dec 10 15:08:08 2023 +0100 @@ -92,7 +92,12 @@ 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" diff -r 6784d07b99c8 -r 50c919dfe61b libervia/backend/plugins/plugin_adhoc_dbus.py --- a/libervia/backend/plugins/plugin_adhoc_dbus.py Sat Dec 09 19:20:13 2023 +0100 +++ b/libervia/backend/plugins/plugin_adhoc_dbus.py Sun Dec 10 15:08:08 2023 +0100 @@ -340,7 +340,7 @@ 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: diff -r 6784d07b99c8 -r 50c919dfe61b libervia/backend/plugins/plugin_xep_0050.py --- a/libervia/backend/plugins/plugin_xep_0050.py Sat Dec 09 19:20:13 2023 +0100 +++ b/libervia/backend/plugins/plugin_xep_0050.py Sun Dec 10 15:08:08 2023 +0100 @@ -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 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 @@ } +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 @@ @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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ @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 @@ 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 @@ 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 @@ __, 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 @@ @implementer(iwokkel.IDisco) class XEP_0050_handler(XMPPHandler): - def __init__(self, plugin_parent): self.plugin_parent = plugin_parent