plugin XEP-0050: use Enums and add type hints:

- Replaces namedtuple with enums which are more appropriate here.
- Add docstrings and type hints to `AdHocCommand`

rel 465
This commit is contained in:
Goffi 2025-10-01 17:57:47 +02:00
commit a32b8bf9e8
5 changed files with 241 additions and 157 deletions

View file

@ -19,6 +19,7 @@
from collections import OrderedDict
import os.path
from typing import cast
import uuid
from twisted.internet import defer
@ -29,6 +30,7 @@ from wokkel import data_form
from libervia.backend.core.constants import Const as C
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.plugins.plugin_xep_0050 import XEP_0050, Action, Error, Note, Status
log = getLogger(__name__)
@ -124,7 +126,7 @@ class AdHocDBus:
method=self._ad_hoc_remotes_get,
async_=True,
)
self._c = host.plugins["XEP-0050"]
self._c = cast(XEP_0050, host.plugins["XEP-0050"])
host.register_namespace("mediaplayer", NS_MEDIA_PLAYER)
self.session_con = None
@ -295,7 +297,7 @@ class AdHocDBus:
if len(actions) == 1:
# it's our first request, we ask the desired new status
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form("form", title=_("Command selection"))
options = []
for path, iface, command in methods:
@ -319,10 +321,10 @@ class AdHocDBus:
answer_form = data_form.Form.fromElement(x_elt)
command = answer_form["command"]
except (KeyError, StopIteration):
raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD)
raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
if command not in names_map:
raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD)
raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
path, iface, command = names_map[command]
proxy = await self.session_con.getRemoteObject(bus_name, path)
@ -337,15 +339,15 @@ class AdHocDBus:
del actions[:]
names_map.clear()
return await d_bus_callback(
client, None, session_data, self._c.ACTION.EXECUTE, node
client, None, session_data, Action.EXECUTE, node
)
form = data_form.Form("form", title=_("Updated"))
form.addField(data_form.Field("fixed", "Command sent"))
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (self._c.NOTE.INFO, _("Command sent"))
note = (Note.INFO, _("Command sent"))
else:
raise self._c.AdHocError(self._c.ERROR.INTERNAL)
raise self._c.ad_hoc_error(Error.INTERNAL)
return (payload, status, None, note)
@ -408,7 +410,7 @@ class AdHocDBus:
client,
device_jid,
NS_MEDIA_PLAYER,
action=self._c.ACTION.CANCEL,
action=Action.CANCEL,
session_id=session_id,
)
@ -458,10 +460,10 @@ class AdHocDBus:
bus_names = await self._dbus_list_names()
bus_names = [b for b in bus_names if b.startswith(MPRIS_PREFIX)]
if len(bus_names) == 0:
note = (self._c.NOTE.INFO, D_("No media player found."))
return (None, self._c.STATUS.COMPLETED, None, note)
note = (Note.INFO, D_("No media player found."))
return (None, Status.COMPLETED, None, note)
options = []
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form(
"form", title=D_("Media Player Selection"), formNamespace=NS_MEDIA_PLAYER
)
@ -491,8 +493,8 @@ class AdHocDBus:
"Hack attempt? Refused bus: {bus_name}"
).format(bus_name=bus_name)
)
note = (self._c.NOTE.ERROR, D_("Invalid player name."))
return (None, self._c.STATUS.COMPLETED, None, note)
note = (Note.ERROR, D_("Invalid player name."))
return (None, Status.COMPLETED, None, note)
try:
proxy = await self.session_con.getRemoteObject(
@ -500,8 +502,8 @@ class AdHocDBus:
)
except Exception as e:
log.warning(_("Can't get D-Bus proxy: {reason}").format(reason=e))
note = (self._c.NOTE.ERROR, D_("Media player is not available anymore"))
return (None, self._c.STATUS.COMPLETED, None, note)
note = (Note.ERROR, D_("Media player is not available anymore"))
return (None, Status.COMPLETED, None, note)
try:
command = command_form["command"]
except KeyError:
@ -543,5 +545,5 @@ class AdHocDBus:
)
payload = form.toElement()
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
return (payload, status, None, None)

View file

@ -18,6 +18,7 @@
import time
from typing import cast
from uuid import uuid4
from twisted.internet import defer
@ -30,6 +31,7 @@ from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory import persistent
from libervia.backend.plugins.plugin_xep_0050 import XEP_0050, Error, Note, Status
from libervia.backend.tools.common import data_format, date_utils
log = getLogger(__name__)
@ -51,7 +53,7 @@ class AdHocRegistration:
def __init__(self, host):
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
self.host = host
self._c = host.plugins["XEP-0050"]
self._c = cast(XEP_0050, host.plugins["XEP-0050"])
self.ad_hoc_registration_data = persistent.LazyPersistentBinaryDict(
"registration_links"
)
@ -134,7 +136,7 @@ class AdHocRegistration:
if len(actions) == 1:
# First request, presenting the form to the user
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form("form", title=D_("Create Registration Link"))
form.addField(
@ -216,18 +218,18 @@ class AdHocRegistration:
await self.ad_hoc_registration_data.aset(link_id, link_data)
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (
self._c.NOTE.INFO,
Note.INFO,
D_("Registration link created successfully: {link_id}").format(
link_id=link_id
),
)
except (KeyError, StopIteration, ValueError):
raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD)
raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
else:
raise self._c.AdHocError(self._c.ERROR.INTERNAL)
raise self._c.ad_hoc_error(Error.INTERNAL)
return (payload, status, None, note)
@ -256,7 +258,7 @@ class AdHocRegistration:
if len(actions) == 1:
all_links = await self.ad_hoc_registration_data.all()
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form("form", title=D_("Registered Links"))
for link_id, link_data in all_links.items():
@ -272,10 +274,10 @@ class AdHocRegistration:
payload = form.toElement()
note = None
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
else:
raise self._c.AdHocError(self._c.ERROR.INTERNAL)
raise self._c.ad_hoc_error(Error.INTERNAL)
return (payload, status, None, note)
@ -305,7 +307,7 @@ class AdHocRegistration:
if len(actions) == 1:
all_links = await self.ad_hoc_registration_data.all()
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form("form", title=D_("Delete Registration Link"))
link_options = [data_form.Option(link_id, link_id) for link_id in all_links]
@ -330,17 +332,17 @@ class AdHocRegistration:
await self.ad_hoc_registration_data.adel(link_to_delete)
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (
self._c.NOTE.INFO,
Note.INFO,
D_("Registration link {link_id} deleted successfully.").format(
link_id=link_to_delete
),
)
except (KeyError, StopIteration, ValueError):
raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD)
raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
else:
raise self._c.AdHocError(self._c.ERROR.INTERNAL)
raise self._c.ad_hoc_error(Error.INTERNAL)
return (payload, status, None, note)

View file

@ -16,6 +16,7 @@
# 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 typing import cast
from twisted.words.protocols.jabber import jid
from twisted.words.xish import domish
from wokkel import data_form
@ -24,6 +25,7 @@ from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.plugins.plugin_xep_0050 import XEP_0050, Note, Status
log = getLogger(__name__)
@ -36,7 +38,7 @@ class APAdHocService:
def __init__(self, apg):
self.host = apg.host
self.apg = apg
self._c = self.host.plugins["XEP-0050"]
self._c = cast(XEP_0050, self.host.plugins["XEP-0050"])
def init(self, client: SatXMPPEntity) -> None:
self._c.add_ad_hoc_command(
@ -62,7 +64,7 @@ class APAdHocService:
command_form = None
if command_form is None or len(command_form.fields) == 0:
# root request
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form(
"form",
title="XMPP JID/node to AP actor conversion",
@ -81,7 +83,7 @@ class APAdHocService:
xmpp_jid = jid.JID(command_form["jid"])
xmpp_node = command_form.get("node")
actor = await self.apg.get_ap_account_from_jid_and_node(xmpp_jid, xmpp_node)
note = (self._c.NOTE.INFO, actor)
status = self._c.STATUS.COMPLETED
note = (Note.INFO, actor)
status = Status.COMPLETED
payload = None
return (payload, status, None, note)

View file

@ -18,6 +18,7 @@
import os.path
from functools import partial
from typing import cast
from wokkel import data_form
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
@ -25,6 +26,8 @@ from libervia.backend.core.i18n import _, D_
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.log import getLogger
from libervia.backend.plugins.plugin_xep_0050 import XEP_0050, Note, Status, Error
from libervia.backend.plugins.plugin_xep_0264 import XEP_0264
from libervia.backend.tools.common import utils
log = getLogger(__name__)
@ -71,8 +74,8 @@ class FileSharingManagement(object):
def __init__(self, host):
log.info(_("File Sharing Management plugin initialization"))
self.host = host
self._c = host.plugins["XEP-0050"]
self._t = host.plugins["XEP-0264"]
self._c = cast(XEP_0050, host.plugins["XEP-0050"])
self._t = cast(XEP_0264, host.plugins["XEP-0264"])
self.files_path = host.get_local_path(None, C.FILES_DIR)
host.bridge.add_method(
"file_sharing_delete",
@ -132,9 +135,9 @@ class FileSharingManagement(object):
@param reason(unicode): reason of the error
@return (tuple): value to return
"""
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (self._c.NOTE.ERROR, reason)
note = (Note.ERROR, reason)
return payload, status, None, note
def _get_root_args(self):
@ -142,7 +145,7 @@ class FileSharingManagement(object):
@return (tuple): value to return
"""
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form(
"form", title="File Management", formNamespace=NS_FILE_MANAGEMENT
)
@ -169,10 +172,10 @@ class FileSharingManagement(object):
path = fields["path"].value.strip()
namespace = fields["namespace"].value or None
except KeyError:
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
if not path:
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
requestor = session_data["requestor"]
requestor_bare = requestor.userhostJID()
@ -302,7 +305,7 @@ class FileSharingManagement(object):
)
form.addField(field)
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
payload = form.toElement()
return (payload, status, None, None)
@ -311,7 +314,7 @@ class FileSharingManagement(object):
try:
read_allowed = command_form.fields["read_allowed"]
except KeyError:
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
if read_allowed.value == "PUBLIC":
allowed_jids = "PUBLIC"
@ -326,7 +329,7 @@ class FileSharingManagement(object):
log.warning(
_("Can't use read_allowed values: {reason}").format(reason=e)
)
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
if found_file["type"] == C.FILE_TYPE_FILE:
await self.host.memory.file_update(
@ -338,7 +341,7 @@ class FileSharingManagement(object):
try:
recursive = command_form.fields["recursive"]
except KeyError:
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
await self.host.memory.file_update(
found_file["id"],
"access",
@ -354,9 +357,9 @@ class FileSharingManagement(object):
)
# job done, we can end the session
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (self._c.NOTE.INFO, _("management session done"))
note = (Note.INFO, _("management session done"))
return (payload, status, None, note)
async def _on_delete_file(self, client, command_elt, session_data, action, node):
@ -403,7 +406,7 @@ class FileSharingManagement(object):
desc="check this box to confirm",
)
form.addField(field)
status = self._c.STATUS.EXECUTING
status = Status.EXECUTING
payload = form.toElement()
return (payload, status, None, None)
@ -412,7 +415,7 @@ class FileSharingManagement(object):
try:
confirmed = C.bool(command_form.fields["confirm"].value)
except KeyError:
self._c.ad_hoc_error(self._c.ERROR.BAD_PAYLOAD)
self._c.ad_hoc_error(Error.BAD_PAYLOAD)
if not confirmed:
note = None
else:
@ -420,8 +423,8 @@ class FileSharingManagement(object):
await self.host.memory.file_delete(
client, requestor_bare, found_file["id"], recursive
)
note = (self._c.NOTE.INFO, _("file deleted"))
status = self._c.STATUS.COMPLETED
note = (Note.INFO, _("file deleted"))
status = Status.COMPLETED
payload = None
return (payload, status, None, note)
@ -501,9 +504,9 @@ class FileSharingManagement(object):
await self._gen_thumbs(client, requestor, found_file["namespace"], found_file)
# job done, we can end the session
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (self._c.NOTE.INFO, _("thumbnails generated"))
note = (Note.INFO, _("thumbnails generated"))
return (payload, status, None, note)
async def _on_quota(self, client, command_elt, session_data, action, node):
@ -513,12 +516,12 @@ class FileSharingManagement(object):
size_used = await self.host.memory.file_get_used_space(client, requestor)
except exceptions.PermissionError:
raise WorkflowError(self._err(_("forbidden")))
status = self._c.STATUS.COMPLETED
status = Status.COMPLETED
form = data_form.Form("result")
form.makeFields({"quota": quota, "user": size_used})
payload = form.toElement()
note = (
self._c.NOTE.INFO,
Note.INFO,
_("You are currently using {size_used} on {size_quota}").format(
size_used=utils.get_human_size(size_used),
size_quota=(

View file

@ -17,24 +17,25 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple
from uuid import uuid4
from enum import Enum, StrEnum, auto
from typing import Callable
from uuid import uuid4
from zope.interface import implementer
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
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
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.core_types import SatXMPPEntity
from wokkel import data_form, disco, iwokkel
from zope.interface import implementer
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory.memory import Sessions
from libervia.backend.tools import xml_tools, utils
from libervia.backend.tools import utils, xml_tools
from libervia.backend.tools.common import data_format
@ -68,50 +69,83 @@ 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 Status(StrEnum):
EXECUTING = auto()
COMPLETED = auto()
CANCELED = auto()
class Action(StrEnum):
EXECUTE = auto()
CANCEL = auto()
NEXT = auto()
PREV = auto()
class Note(StrEnum):
INFO = auto()
WARN = auto()
ERROR = auto()
class Error(tuple[str, str | None], Enum):
"""Errors from XEP-0050 §4.4 Table 5"""
MALFORMED_ACTION = ("bad-request", "malformed-action")
BAD_ACTION = ("bad-request", "bad-action")
BAD_LOCALE = ("bad-request", "bad-locale")
BAD_PAYLOAD = ("bad-request", "bad-payload")
BAD_SESSIONID = ("bad-request", "bad-sessionid")
SESSION_EXPIRED = ("not-allowed", "session-expired")
FORBIDDEN = ("forbidden", None)
ITEM_NOT_FOUND = ("item-not-found", None)
FEATURE_NOT_IMPLEMENTED = ("feature-not-implemented", None)
INTERNAL = ("internal-server-error", None)
class AdHocError(Exception):
def __init__(self, error_const):
"""Error to be used from callback
@param error_const: one of XEP_0050.ERROR
@param error_const: one of Error
"""
assert error_const in XEP_0050.ERROR
assert error_const in Error
self.callback_error = error_const
@implementer(iwokkel.IDisco)
class AdHocCommand(XMPPHandler):
"""Implementation of an Ad-Hoc Command handler for XMPP.
This class handles the execution of ad-hoc commands as defined in XEP-0050.
It manages command sessions, authorization checks, and communication with clients.
"""
def __init__(
self,
callback,
label,
node,
features,
timeout,
allowed_jids,
allowed_groups,
allowed_magics,
forbidden_jids,
forbidden_groups,
):
callback: Callable,
label: str,
node: str,
features: list[str],
timeout: int,
allowed_jids: list[jid.JID],
allowed_groups: list[str],
allowed_magics: list[str],
forbidden_jids: list[jid.JID],
forbidden_groups: list[str],
) -> None:
"""Initialize the AdHocCommand handler.
@param callback: The callback function to execute when command is invoked
@param label: Label for the command displayed in UI
@param node: Unique identifier for this command
@param features: List of features supported by this command
@param timeout: Session timeout in seconds
@param allowed_jids: List of JIDs that are allowed to use this command
@param allowed_groups: List of roster groups that are allowed to use this command
@param allowed_magics: List of magic identifiers for special access rules
@param forbidden_jids: List of JIDs that are forbidden from using this command
@param forbidden_groups: List of roster groups that are forbidden from using this
command
"""
XMPPHandler.__init__(self)
self.callback = callback
self.label = label
@ -125,13 +159,28 @@ class AdHocCommand(XMPPHandler):
self.sessions = Sessions(timeout=timeout)
@property
def client(self):
def client(self) -> SatXMPPEntity:
"""Get the client associated with this command handler.
@return: The XMPP client instance
"""
assert self.parent is not None
return self.parent
def getName(self, xml_lang=None):
def getName(self, xml_lang: str | None = None) -> str:
"""Get the name of this command for display purposes.
@param xml_lang: Language code for localization (unused)
@return: The label of this command
"""
return self.label
def is_authorised(self, requestor):
def is_authorised(self, requestor: jid.JID) -> bool:
"""Check if a requestor is authorized to use this command.
@param requestor: JID of the entity requesting access
@return: True if authorized, False otherwise
"""
if C.ENTITY_ALL in self.allowed_magics:
return True
forbidden = set(self.forbidden_jids)
@ -153,7 +202,16 @@ class AdHocCommand(XMPPHandler):
return True
return False
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
def getDiscoInfo(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoFeature]:
"""Get disco info for this command.
@param requestor: JID of the entity requesting information
@param target: Target JID
@param nodeIdentifier: Node identifier (unused)
@return: List of disco features supported by this command
"""
if (
nodeIdentifier != NS_COMMANDS
): # FIXME: we should manage other disco nodes here
@ -161,38 +219,46 @@ class AdHocCommand(XMPPHandler):
# identities = [ID_CMD_LIST if self.node == NS_COMMANDS else ID_CMD_NODE] # FIXME
return [disco.DiscoFeature(NS_COMMANDS)] + self.features
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
def getDiscoItems(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoItem]:
"""Get disco items for this command.
@param requestor: JID of the entity requesting information
@param target: Target JID
@param nodeIdentifier: Node identifier (unused)
@return: Empty list as no items are supported
"""
return []
def _sendAnswer(
self,
callback_data: tuple[
tuple[domish.Element, None],
domish.Element | None,
Status,
list[str] | None,
tuple[Note, str] | None,
],
session_id,
request,
session_id: str,
request: domish.Element,
) -> None:
"""Send result of the command
"""Send result of the command.
@param callback_data: tuple (payload, status, actions, note) with:
- payload: usualy containing data form
- payload: usually containing data form
- status: current status
- actions: list of allowed actions (see XEP_0050.ACTION).
- actions: list of allowed actions (see 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
"""
payload, status, actions, note = callback_data
assert isinstance(payload, domish.Element) or payload is None
assert status in XEP_0050.STATUS
assert status in Status
if not actions:
actions = [XEP_0050.ACTION.EXECUTE]
actions = [Action.EXECUTE]
result = domish.Element((None, "iq"))
result["type"] = "result"
result["id"] = request["id"]
@ -202,8 +268,8 @@ class AdHocCommand(XMPPHandler):
command_elt["node"] = self.node
command_elt["status"] = status
if status != XEP_0050.STATUS.CANCELED:
if status != XEP_0050.STATUS.COMPLETED:
if status != Status.CANCELED:
if status != Status.COMPLETED:
actions_elt = command_elt.addElement("actions")
actions_elt["execute"] = actions[0]
for action in actions:
@ -218,13 +284,19 @@ class AdHocCommand(XMPPHandler):
command_elt.addChild(payload)
self.client.send(result)
if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED):
if status in (Status.COMPLETED, Status.CANCELED):
del self.sessions[session_id]
def _sendError(self, error_constant, session_id, request):
"""Send error stanza
def _sendError(
self,
error_constant: tuple[str, str | None],
session_id: str,
request: domish.Element,
) -> None:
"""Send error stanza.
@param error_constant: one of XEP_OO50.ERROR
@param error_constant: one of Error
@param session_id: Session ID
@param request: original request (domish.Element)
"""
xmpp_condition, cmd_condition = error_constant
@ -235,36 +307,54 @@ class AdHocCommand(XMPPHandler):
self.client.send(iq_elt)
del self.sessions[session_id]
def _request_eb(self, failure_, request, session_id):
def _request_eb(
self, failure_: defer.Failure, request: domish.Element, session_id: str
) -> None:
"""Error handler for command requests.
@param failure_: The error that occurred
@param request: Original request (domish.Element)
@param session_id: Session ID
"""
if failure_.check(AdHocError):
error_constant = failure_.value.callback_error
else:
log.error(f"unexpected error while handling request: {failure_}")
error_constant = XEP_0050.ERROR.INTERNAL
error_constant = Error.INTERNAL
self._sendError(error_constant, session_id, request)
def on_request(self, command_elt, requestor, action, session_id):
def on_request(
self,
command_elt: domish.Element,
requestor: jid.JID,
action: str,
session_id: str | None,
) -> None:
"""Handle an incoming command request.
@param command_elt: The command element from the request
@param requestor: JID of the entity making the request
@param action: The action to perform (execute, cancel, etc.)
@param session_id: Session ID if continuing an existing session
@return: Deferred for handling the response
"""
if not self.is_authorised(requestor):
return self._sendError(
XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent
)
return self._sendError(Error.FORBIDDEN, session_id, command_elt.parent)
if session_id:
try:
session_data = self.sessions[session_id]
except KeyError:
return self._sendError(
XEP_0050.ERROR.SESSION_EXPIRED, session_id, command_elt.parent
Error.SESSION_EXPIRED, session_id, command_elt.parent
)
if session_data["requestor"] != requestor:
return self._sendError(
XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent
)
return self._sendError(Error.FORBIDDEN, session_id, command_elt.parent)
else:
session_id, session_data = self.sessions.new_session()
session_data["requestor"] = requestor
if action == XEP_0050.ACTION.CANCEL:
d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None))
if action == Action.CANCEL:
d = defer.succeed((None, Status.CANCELED, None, None))
else:
d = utils.as_deferred(
self.callback,
@ -279,21 +369,6 @@ class AdHocCommand(XMPPHandler):
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"),
("bad-request", "bad-payload"),
("bad-request", "bad-sessionid"),
("not-allowed", "session-expired"),
("forbidden", None),
("item-not-found", None),
("feature-not-implemented", None),
("internal-server-error", None),
) # XEP-0050 §4.4 Table 5
def __init__(self, host):
log.info(_("plugin XEP-0050 initialization"))
@ -357,7 +432,7 @@ class XEP_0050:
@param entity(jid.JID): entity which will execture the command
@param node(unicode): node of the command
@param action(unicode): one of XEP_0050.ACTION
@param action(unicode): one of Action
@param session_id(unicode, None): id of the ad-hoc session
None if no session is involved
@param form_values(dict, None): values to use to create command form
@ -386,10 +461,10 @@ class XEP_0050:
except StopIteration:
raise exceptions.NotFound(_("Missing command element"))
def ad_hoc_error(self, error_type):
def ad_hoc_error(self, error_type: Error):
"""Shortcut to raise an AdHocError
@param error_type(unicode): one of XEP_0050.ERROR
@param error_type(unicode): one of Error
"""
raise AdHocError(error_type)
@ -436,7 +511,7 @@ class XEP_0050:
def parse_command_answer(self, iq_elt):
command_elt = self.get_command_elt(iq_elt)
data = {}
data["status"] = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING)
data["status"] = command_elt.getAttribute("status", Status.EXECUTING)
data["session_id"] = command_elt.getAttribute("sessionid")
data["notes"] = notes = []
for note_elt in command_elt.elements(NS_COMMANDS, "note"):
@ -458,10 +533,10 @@ class XEP_0050:
"""
command_elt, answer_data = self.parse_command_answer(iq_elt)
status = answer_data["status"]
if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]:
if status in [Status.COMPLETED, Status.CANCELED]:
# the command session is finished, we purge our session
del self.requesting[session_id]
if status == XEP_0050.STATUS.COMPLETED:
if status == Status.COMPLETED:
session_id = None
else:
return None
@ -474,7 +549,7 @@ class XEP_0050:
break
else:
# no matching data element found
if status != XEP_0050.STATUS.COMPLETED:
if status != Status.COMPLETED:
log.warning(
_("No known payload found in ad-hoc command result, aborting")
)
@ -586,7 +661,7 @@ class XEP_0050:
client,
entity,
session_data["node"],
action=XEP_0050.ACTION.EXECUTE,
action=Action.EXECUTE,
session_id=remote_id,
form_values=data_form_values,
)
@ -614,7 +689,7 @@ class XEP_0050:
if len(actions) == 1:
# it's our first request, we ask the desired new status
status = XEP_0050.STATUS.EXECUTING
status = Status.EXECUTING
form = data_form.Form("form", title=_("status selection"))
show_options = [
data_form.Option(name, label) for name, label in list(SHOWS.items())
@ -634,20 +709,20 @@ class XEP_0050:
answer_form = data_form.Form.fromElement(x_elt)
show = answer_form["show"]
except (KeyError, StopIteration):
self.ad_hoc_error(XEP_0050.ERROR.BAD_PAYLOAD)
self.ad_hoc_error(Error.BAD_PAYLOAD)
if show not in SHOWS:
self.ad_hoc_error(XEP_0050.ERROR.BAD_PAYLOAD)
self.ad_hoc_error(Error.BAD_PAYLOAD)
if show == "disconnect":
self.host.disconnect(client.profile)
else:
self.host.presence_set(show=show, profile_key=client.profile)
# job done, we can end the session
status = XEP_0050.STATUS.COMPLETED
status = Status.COMPLETED
payload = None
note = (self.NOTE.INFO, _("Status updated"))
note = (Note.INFO, _("Status updated"))
else:
self.ad_hoc_error(XEP_0050.ERROR.INTERNAL)
self.ad_hoc_error(Error.INTERNAL)
return (payload, status, None, note)