diff --git a/libervia/backend/memory/memory.py b/libervia/backend/memory/memory.py
index fceb0271..c37765cb 100644
--- a/libervia/backend/memory/memory.py
+++ b/libervia/backend/memory/memory.py
@@ -24,7 +24,7 @@ import mimetypes
import os.path
from pathlib import Path
import time
-from typing import Any, Dict, NamedTuple, Optional, Tuple
+from typing import Any, Callable, Dict, NamedTuple, Optional, Tuple
from uuid import uuid4
import shortuuid
@@ -144,7 +144,7 @@ class Sessions:
"You need to use __getitem__ when profile is not set"
)
except KeyError:
- raise failure.Failure(KeyError(MSG_NO_SESSION))
+ raise KeyError(MSG_NO_SESSION)
if profile_set != profile:
raise exceptions.InternalError("current profile differ from set profile !")
if self.resettable_timeout:
@@ -155,7 +155,7 @@ class Sessions:
try:
session_info = self._sessions[session_id]
except KeyError:
- raise failure.Failure(KeyError(MSG_NO_SESSION))
+ raise KeyError(MSG_NO_SESSION)
if session_info.profile is not None:
raise exceptions.InternalError(
"You need to use profile_get instead of __getitem__ when profile is set."
@@ -1620,7 +1620,7 @@ class Memory:
if remaining_path_elts:
# if we have remaining path elements,
# the parent directory is not found
- raise failure.Failure(exceptions.NotFound())
+ raise exceptions.NotFound()
if parent and peer_jid:
# if parent is given directly and permission check is requested,
# we need to check all the parents
@@ -1823,13 +1823,15 @@ class Memory:
raise exceptions.PermissionError("You are not allowed to check this size")
return await self.storage.file_get_used_space(client, owner)
- def file_update(self, file_id, column, update_cb):
+ def file_update(
+ self, file_id: str, column: str, update_cb: Callable[[dict], None]
+ ) -> Any:
"""Update a file column taking care of race condition
access is NOT checked in this method, it must be checked beforehand
- @param file_id(unicode): id of the file to update
- @param column(unicode): one of "access" or "extra"
- @param update_cb(callable): method to update the value of the colum
+ @param file_id: id of the file to update
+ @param column: one of "access" or "extra"
+ @param update_cb: method to update the value of the colum
the method will take older value as argument, and must update it in place
Note that the callable must be thread-safe
"""
diff --git a/libervia/backend/plugins/plugin_adhoc_dbus.py b/libervia/backend/plugins/plugin_adhoc_dbus.py
index 3d9dffd6..c22de126 100644
--- a/libervia/backend/plugins/plugin_adhoc_dbus.py
+++ b/libervia/backend/plugins/plugin_adhoc_dbus.py
@@ -26,23 +26,22 @@ import uuid
from twisted.internet import defer
from twisted.internet import reactor
from twisted.words.protocols.jabber import jid
-from twisted.words.xish import domish
from wokkel import data_form
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 SessionData
-from libervia.backend.plugins.plugin_xep_0004 import DataForm
+from libervia.backend.plugins.plugin_xep_0004 import DataForm, ListSingle, Option, Text
from libervia.backend.plugins.plugin_xep_0050 import (
XEP_0050,
Action,
AdHocCallbackData,
+ AdHocCommand,
AdHocError,
Error,
Note,
- NoteType,
+ PageData,
Status,
)
@@ -97,7 +96,7 @@ MPRIS_PROPERTIES = OrderedDict(
)
)
MPRIS_METADATA_KEY = "Metadata"
-MPRIS_METADATA_MAP = OrderedDict((("xesam:title", "Title"),))
+MPRIS_METADATA_MAP = {"xesam:title": "Title"}
INTROSPECT_METHOD = "Introspect"
IGNORED_IFACES_START = (
@@ -152,13 +151,15 @@ class AdHocDBus:
self.session_con = await dbus_client.connect(reactor, "session")
self.fd_object = await self.session_con.getRemoteObject(FD_NAME, FD_PATH)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self.local_media_cb,
- D_("Media Players"),
- node=NS_MEDIA_PLAYER,
- timeout=60 * 60 * 6, # 6 hours timeout, to avoid breaking remote
- # in the middle of a movie
+ AdHocCommand(
+ callback=self.local_media_cb,
+ label=D_("Media Players"),
+ node=NS_MEDIA_PLAYER,
+ # 6 hours timeout, to avoid breaking remote, in the middle of a movie
+ timeout=60 * 60 * 6,
+ ),
)
async def _dbus_async_call(self, proxy, method, *args, **kwargs):
@@ -275,17 +276,15 @@ class AdHocDBus:
await self._introspect(methods, bus_name, proxy)
if methods:
- self._add_command(
+ self._c.add_ad_hoc_command(
client,
+ partial(self.dbus_callback, methods, bus_name, flags or set()),
prog_name,
- bus_name,
- methods,
allowed_jids=allowed_jids,
allowed_groups=allowed_groups,
allowed_magics=allowed_magics,
forbidden_jids=forbidden_jids,
forbidden_groups=forbidden_groups,
- flags=flags,
)
return (str(bus_name), methods)
@@ -296,10 +295,7 @@ class AdHocDBus:
bus_name: str,
flags: set[str],
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Ad-hoc command for executing D-Bus methods.
@@ -307,49 +303,41 @@ class AdHocDBus:
@param bus_name: Name of the D-Bus service.
@param flags: Command execution flags.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: When bad payload or internal errors occur.
"""
- actions = session_data.setdefault("actions", [])
+ session_data = page_data.session_data
names_map = session_data.setdefault("names_map", {})
- actions.append(action)
- if len(actions) == 1:
- # it's our first request, we ask the desired new status
- status = Status.EXECUTING
- wokkel_form = data_form.Form("form", title=_("Command selection"))
+ if page_data.idx == 0:
+ # It's our first request, we ask the desired new status.
+ session_data["restart"] = False
+ form = DataForm(title=D_("Command selection"))
options = []
for path, iface, command in methods:
label = command.rsplit(".", 1)[-1]
- name = str(uuid.uuid4())
- names_map[name] = (path, iface, command)
- options.append(data_form.Option(name, label))
+ value = str(uuid.uuid4())
+ names_map[value] = (path, iface, command)
+ options.append(Option(value=value, label=label))
- field = data_form.Field(
- "list-single", "command", options=options, required=True
- )
- wokkel_form.addField(field)
+ form.fields.append(ListSingle(var="command", options=options, required=True))
+ session_data["command_form"] = form
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=[]
- )
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
- elif len(actions) == 2:
- # we should have the answer here
+ else:
+ # We should have the form here.
try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- answer_form = data_form.Form.fromElement(x_elt)
- command = answer_form["command"]
- except (KeyError, StopIteration):
- raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
+ command = page_data.form.get_field("command", ListSingle).value
+ if not command:
+ raise KeyError
+ except KeyError:
+ raise AdHocError(Error.BAD_PAYLOAD)
if command not in names_map:
- raise self._c.ad_hoc_error(Error.BAD_PAYLOAD)
+ raise AdHocError(Error.BAD_PAYLOAD, text=f"Invalid command: {command!r}.")
path, iface, command = names_map[command]
assert self.session_con is not None
@@ -357,63 +345,29 @@ class AdHocDBus:
await self._dbus_async_call(proxy, command, interface=iface)
- # job done, we can end the session, except if we have FLAG_LOOP
+ # Job done, we can end the session, except if we have FLAG_LOOP.
if FLAG_LOOP in flags:
- # We have a loop, so we clear everything and we execute again the command
- # as we had a first call (command_elt is not used).
- del actions[:]
- names_map.clear()
- return await self.dbus_callback(
- methods,
- bus_name,
- flags,
- client,
- command_elt,
- session_data,
- Action.EXECUTE,
- node,
+ return AdHocCallbackData(
+ form=session_data["command_form"],
+ status=Status.EXECUTING,
+ notes=[Note(text=D_("Command sent."))],
+ )
+ else:
+ return AdHocCallbackData(
+ status=Status.COMPLETED, notes=[Note(text=D_("Command sent."))]
)
- return AdHocCallbackData(
- status=Status.COMPLETED, notes=[Note(text=_("Command sent"))]
- )
- else:
- raise AdHocError(Error.INTERNAL)
-
- def _add_command(
- self,
- client,
- adhoc_name,
- bus_name,
- methods,
- allowed_jids=None,
- allowed_groups=None,
- allowed_magics=None,
- forbidden_jids=None,
- forbidden_groups=None,
- flags=None,
- ):
- if flags is None:
- flags = set()
-
- self._c.add_ad_hoc_command(
- client,
- partial(self.dbus_callback, methods, bus_name, flags),
- adhoc_name,
- allowed_jids=allowed_jids,
- allowed_groups=allowed_groups,
- allowed_magics=allowed_magics,
- forbidden_jids=forbidden_jids,
- forbidden_groups=forbidden_groups,
- )
## Local media ##
def _ad_hoc_remotes_get(self, profile):
return self.ad_hoc_remotes_get(self.host.get_client(profile))
- async def ad_hoc_remotes_get(self, client):
- """Retrieve available remote media controlers in our devices
- @return (list[tuple[unicode, unicode, unicode]]): list of devices with:
+ async def ad_hoc_remotes_get(
+ self, client: SatXMPPEntity
+ ) -> list[tuple[str, str, str]]:
+ """Retrieve available remote media controlers in our devices.
+
+ @return: list of devices with:
- entity full jid
- device name
- device label
@@ -484,134 +438,112 @@ class AdHocDBus:
args = []
return await self._dbus_async_call(proxy, command, *args, interface=iface)
- def add_mpris_metadata(self, form, metadata):
+ def add_mpris_metadata(self, form: DataForm, metadata: dict):
"""Serialise MRPIS Metadata according to MPRIS_METADATA_MAP"""
for mpris_key, name in MPRIS_METADATA_MAP.items():
if mpris_key in metadata:
value = str(metadata[mpris_key])
- form.addField(data_form.Field(fieldType="fixed", var=name, value=value))
+ form.fields.append(Text(type="fixed", var=name, value=value))
async def local_media_cb(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Ad-hoc command for controlling media players via D-Bus.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
- @raise ValueError: When media_player value is missing.
- @raise AdHocError: When invalid player name or other internal errors occur.
+
+ @raise AdHocError: Something went wrong.
"""
assert txdbus is not None
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- command_form = data_form.Form.fromElement(x_elt)
- except StopIteration:
- command_form = None
- if command_form is None or len(command_form.fields) == 0:
+ if page_data.idx == 0:
# root request, we looks for media players
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:
return AdHocCallbackData(
status=Status.COMPLETED,
- notes=[Note(type=NoteType.INFO, text=D_("No media player found."))],
+ notes=[Note(text=D_("No media player found."))],
)
options = []
- status = Status.EXECUTING
- wokkel_form = data_form.Form(
- "form", title=D_("Media Player Selection"), formNamespace=NS_MEDIA_PLAYER
+ form = DataForm(
+ title=D_("Media Player Selection"),
+ namespace=NS_MEDIA_PLAYER,
)
for bus in bus_names:
player_name = bus[len(MPRIS_PREFIX) + 1 :]
if not player_name:
- log.warning(_("Ignoring MPRIS bus without suffix"))
+ log.warning(_("Ignoring MPRIS bus without suffix."))
continue
- options.append(data_form.Option(bus, player_name))
- field = data_form.Field(
- "list-single", "media_player", options=options, required=True
- )
- wokkel_form.addField(field)
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=[]
+ options.append(Option(value=bus, label=player_name))
+ form.fields.append(
+ ListSingle(var="media_player", options=options, required=True)
)
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
else:
# player request
try:
- bus_name = command_form["media_player"]
+ bus_name = page_data.session_data["bus_name"]
except KeyError:
- raise ValueError(_("missing media_player value"))
+ try:
+ bus_name = page_data.form.get_field("media_player", ListSingle).value
+ if not bus_name:
+ raise KeyError
+ except KeyError:
+ raise AdHocError(
+ Error.BAD_PAYLOAD, text=D_("missing media_player value")
+ )
+ else:
+ page_data.session_data["bus_name"] = bus_name
if not bus_name.startswith(MPRIS_PREFIX):
log.warning(
- _(
- "Media player ad-hoc command trying to use non MPRIS bus. "
- "Hack attempt? Refused bus: {bus_name}"
- ).format(bus_name=bus_name)
+ "Media player ad-hoc command trying to use non MPRIS bus. Hack "
+ f"attempt? Refused bus: {bus_name}"
)
- note = Note(type=NoteType.ERROR, text=D_("Invalid player name."))
- return AdHocCallbackData(status=Status.COMPLETED, notes=[note])
-
+ raise AdHocError(Error.BAD_PAYLOAD, text=D_("Invalid player name."))
+ assert self.session_con is not None
try:
proxy = await self.session_con.getRemoteObject(
bus_name, MPRIS_PATH, "org.mpris.MediaPlayer2.Player"
)
except Exception as e:
- log.warning(_("Can't get D-Bus proxy: {reason}").format(reason=e))
- note = Note(
- type=NoteType.ERROR, text=D_("Media player is not available anymore")
+ log.exception(f"Can't get D-Bus proxy.")
+ raise AdHocError(
+ Error.ITEM_NOT_FOUND, D_("Media player is not available anymore")
)
- return AdHocCallbackData(status=Status.COMPLETED, notes=[note])
try:
- command = command_form["command"]
+ command = page_data.form.get_field("command", ListSingle).value
+ if not command:
+ raise KeyError
except KeyError:
pass
else:
await self.do_mpris_command(proxy, command)
# we construct the remote control form
- wokkel_form = data_form.Form("form", title=D_("Media Player Selection"))
- wokkel_form.addField(
- data_form.Field(fieldType="hidden", var="media_player", value=bus_name)
+ form = DataForm(
+ title=D_("Media Player Selection"),
)
for iface, properties_names in MPRIS_PROPERTIES.items():
for name in properties_names:
try:
value = await self._dbus_get_property(proxy, iface, name)
except Exception as e:
- log.warning(
- _("Can't retrieve attribute {name}: {reason}").format(
- name=name, reason=e
- )
- )
+ log.warning(f"Can't retrieve attribute {name}: {e}")
continue
if name == MPRIS_METADATA_KEY:
- self.add_mpris_metadata(wokkel_form, value)
+ self.add_mpris_metadata(form, value)
else:
- wokkel_form.addField(
- data_form.Field(fieldType="fixed", var=name, value=str(value))
- )
+ form.fields.append(Text(type="fixed", var=name, value=str(value)))
- commands = [data_form.Option(c, c.rsplit(".", 1)[1]) for c in MPRIS_COMMANDS]
- wokkel_form.addField(
- data_form.Field(
- fieldType="list-single",
- var="command",
- options=commands,
- required=True,
- )
- )
+ commands = [
+ Option(value=c, label=c.rsplit(".", 1)[1]) for c in MPRIS_COMMANDS
+ ]
+ form.fields.append(ListSingle(var="command", options=commands, required=True))
- status = Status.EXECUTING
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=[]
- )
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
diff --git a/libervia/backend/plugins/plugin_adhoc_registration.py b/libervia/backend/plugins/plugin_adhoc_registration.py
index a04ea8f2..e55a5b5c 100644
--- a/libervia/backend/plugins/plugin_adhoc_registration.py
+++ b/libervia/backend/plugins/plugin_adhoc_registration.py
@@ -22,8 +22,6 @@ from typing import cast
from uuid import uuid4
from twisted.internet import defer
-from twisted.words.xish import domish
-from wokkel import data_form as wokkel_data_form
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
@@ -31,15 +29,15 @@ 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.memory.memory import SessionData
-from libervia.backend.plugins.plugin_xep_0004 import DataForm
+from libervia.backend.plugins.plugin_xep_0004 import DataForm, ListSingle, Option, Text
from libervia.backend.plugins.plugin_xep_0050 import (
XEP_0050,
AdHocCallbackData,
+ AdHocCommand,
AdHocError,
Error,
Note,
- NoteType,
+ PageData,
Status,
)
from libervia.backend.tools.common import data_format, date_utils
@@ -100,32 +98,70 @@ class AdHocRegistration:
async def profile_connected(self, client):
if client.is_admin:
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self.create_registration_link,
- D_("Create Registration Link"),
- node="https://libervia.org/registration/create",
+ AdHocCommand(
+ callback=self.create_registration_link,
+ label=D_("Create Registration Link"),
+ node="https://libervia.org/registration/create",
+ form=DataForm(
+ title=D_("Create Registration Link"),
+ fields=[
+ Text(var="recipient", label=D_("Recipient Name")),
+ Text(
+ var="expiration_time_pattern",
+ label=D_("Expiration Date"),
+ desc=D_(
+ "Set the expiry duration for this link. Use the"
+ " Libervia Time Pattern (e.g., '1 week'). The link"
+ " will expire after this period."
+ ),
+ value="1 week",
+ required=True,
+ ),
+ Text(
+ var="registration_limit",
+ label=D_("Maximum registrations limit"),
+ desc=D_(
+ "How many accounts can be registered using this link."
+ " Set to 0 for unlimited registrations."
+ ),
+ value="1",
+ required=True,
+ ),
+ Text(
+ type="text-multi",
+ var="welcome_message",
+ label=D_("Welcome Message"),
+ ),
+ Text(
+ type="text-multi",
+ var="extra_details",
+ label=D_("Additional Details"),
+ ),
+ ],
+ ),
+ ),
)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self.list_registration_links,
- D_("List Registration Links"),
- node="https://libervia.org/registration/list",
+ AdHocCommand(
+ callback=self.list_registration_links,
+ label=D_("List Registration Links"),
+ node="https://libervia.org/registration/list",
+ ),
)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self.delete_registration_link,
- D_("Delete Registration Link"),
- node="https://libervia.org/registration/delete",
+ AdHocCommand(
+ callback=self.delete_registration_link,
+ label=D_("Delete Registration Link"),
+ node="https://libervia.org/registration/delete",
+ ),
)
async def create_registration_link(
- self,
- client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ self, client: SatXMPPEntity, page_data: PageData
) -> AdHocCallbackData:
"""Ad-hoc command used to create a registration link.
@@ -134,175 +170,91 @@ class AdHocRegistration:
associated data.
@param client: The XMPP client instance.
- @param command_elt: The command element.
- @param session_data: Data associated with the current session.
- @param action: The action being performed.
- @param node: The node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
+
@raise AdHocError: If the payload is invalid or internal error occurs.
"""
- actions = session_data.setdefault("actions", [])
- actions.append(action)
-
- if len(actions) == 1:
- # First request, presenting the form to the user
- status = Status.EXECUTING
- form = wokkel_data_form.Form("form", title=D_("Create Registration Link"))
-
- form.addField(
- wokkel_data_form.Field(
- "text-single", "recipient", label=D_("Recipient Name")
+ form = page_data.form
+ try:
+ recipient = form.get_field("recipient", Text).value
+ expiration_time_pattern = form.get_field(
+ "expiration_time_pattern", Text
+ ).value
+ if not expiration_time_pattern:
+ raise KeyError
+ if expiration_time_pattern == "never":
+ expiration_timestamp = None
+ else:
+ expiration_timestamp = date_utils.date_parse_ext(
+ expiration_time_pattern, default_tz=date_utils.TZ_LOCAL
)
- )
- form.addField(
- wokkel_data_form.Field(
- "text-single",
- "expiration_time_pattern",
- label=D_("Expiration Date"),
- desc=D_(
- "Set the expiry duration for this link. Use the Libervia Time "
- "Pattern (e.g., '1 week'). The link will expire after this "
- "period."
+ registration_limit = form.get_field("registration_limit", Text).value
+ if registration_limit is None:
+ raise KeyError
+ registration_limit = int(registration_limit)
+
+ welcome_message = form.get_field("welcome_message", Text).value or ""
+ extra_details = form.get_field("extra_details", Text).value or ""
+
+ link_id = str(uuid4())
+
+ link_data = {
+ "recipient": recipient,
+ "registration_limit": registration_limit,
+ "welcome_message": welcome_message,
+ "extra_details": extra_details,
+ "creator_jid": page_data.requestor.full(),
+ "use_count": 0,
+ "creation_timestamp": int(time.time()),
+ }
+ if expiration_timestamp is not None:
+ link_data["expiration_timestamp"] = expiration_timestamp
+
+ await self.ad_hoc_registration_data.aset(link_id, link_data)
+
+ notes = [
+ Note(
+ text=D_("Registration link created successfully: {link_id}").format(
+ link_id=link_id
),
- value="1 week",
- required=True,
)
- )
- form.addField(
- wokkel_data_form.Field(
- "text-single",
- "registration_limit",
- label=D_("Maximum registrations limit"),
- desc=D_(
- "How many accounts can be registered using this link. Set to 0 "
- "for unlimited registrations."
- ),
- value="1",
- required=True,
- )
- )
- form.addField(
- wokkel_data_form.Field(
- "text-multi", "welcome_message", label=D_("Welcome Message")
- )
- )
- form.addField(
- wokkel_data_form.Field(
- "text-multi",
- "extra_details",
- label=D_("Additional Details"),
- )
- )
+ ]
+ except (KeyError, ValueError):
+ raise AdHocError(Error.BAD_PAYLOAD)
- payload = form.toElement()
- notes = []
-
- elif len(actions) == 2:
- try:
- x_elt = next(command_elt.elements(wokkel_data_form.NS_X_DATA, "x"))
- answer_form = wokkel_data_form.Form.fromElement(x_elt)
-
- recipient = answer_form.get("recipient", None)
- expiration_time_pattern = answer_form["expiration_time_pattern"]
- if expiration_time_pattern == "never":
- expiration_timestamp = None
- else:
- expiration_timestamp = date_utils.date_parse_ext(
- expiration_time_pattern, default_tz=date_utils.TZ_LOCAL
- )
- registration_limit = int(answer_form["registration_limit"])
- welcome_message = answer_form.get("welcome_message", "")
- extra_details = answer_form.get("extra_details", "")
-
- link_id = str(uuid4())
-
- link_data = {
- "recipient": recipient,
- "registration_limit": registration_limit,
- "welcome_message": welcome_message,
- "extra_details": extra_details,
- "creator_jid": session_data["requestor"].full(),
- "use_count": 0,
- "creation_timestamp": int(time.time()),
- }
- if expiration_timestamp is not None:
- link_data["expiration_timestamp"] = expiration_timestamp
-
- await self.ad_hoc_registration_data.aset(link_id, link_data)
-
- status = Status.COMPLETED
- payload = None
- notes = [
- Note(
- type=NoteType.INFO,
- text=D_(
- "Registration link created successfully: {link_id}"
- ).format(link_id=link_id),
- )
- ]
- except (KeyError, StopIteration, ValueError):
- raise AdHocError(Error.BAD_PAYLOAD)
- else:
- raise AdHocError(Error.INTERNAL)
-
- return AdHocCallbackData(payload=payload, status=status, notes=notes)
+ return AdHocCallbackData(status=Status.COMPLETED, notes=notes)
async def list_registration_links(
- self,
- client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ self, client: SatXMPPEntity, page_data: PageData
) -> AdHocCallbackData:
"""Ad-hoc command used to list all registration links.
This method retrieves all the registration links and presents them to the user.
@param client: The XMPP client instance.
- @param command_elt: The command element.
- @param session_data: Data associated with the current session.
- @param action: The action being performed.
- @param node: The node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
+
@raise AdHocError: If internal error occurs.
"""
- actions = session_data.setdefault("actions", [])
- actions.append(action)
+ all_links = await self.ad_hoc_registration_data.all()
- if len(actions) == 1:
- all_links = await self.ad_hoc_registration_data.all()
- status = Status.EXECUTING
-
- wokkel_form = wokkel_data_form.Form("form", title=D_("Registered Links"))
- for link_id, link_data in all_links.items():
- wokkel_form.addField(
- wokkel_data_form.Field(
- "text-multi",
- var=f"link_{link_id}",
- label=D_("Link ID: {link_id}").format(link_id=link_id),
- value=str(link_data),
- )
+ form = DataForm(type="result", title=D_("Registered Links"))
+ for link_id, link_data in all_links.items():
+ form.fields.append(
+ Text(
+ type="text-multi",
+ var=f"link_{link_id}",
+ label=D_("Link ID: {link_id}").format(link_id=link_id),
+ value=str(link_data),
)
+ )
- notes = []
-
- status = Status.COMPLETED
-
- else:
- raise AdHocError(Error.INTERNAL)
-
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=notes
- )
+ return AdHocCallbackData(form=form, status=Status.COMPLETED)
async def delete_registration_link(
- self,
- client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ self, client: SatXMPPEntity, page_data: PageData
) -> AdHocCallbackData:
"""Ad-hoc command used to delete a registration link.
@@ -310,60 +262,40 @@ class AdHocRegistration:
delete, and processes the submitted form to delete the selected link.
@param client: The XMPP client instance.
- @param command_elt: The command element.
- @param session_data: Data associated with the current session.
- @param action: The action being performed.
- @param node: The node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
+
@raise AdHocError: If the payload is invalid or internal error occurs.
"""
- actions = session_data.setdefault("actions", [])
- actions.append(action)
-
- if len(actions) == 1:
+ if page_data.idx == 0:
all_links = await self.ad_hoc_registration_data.all()
- status = Status.EXECUTING
- wokkel_form = wokkel_data_form.Form(
- "form", title=D_("Delete Registration Link")
+ form = DataForm(
+ title=D_("Delete Registration Link"),
)
- link_options = [
- wokkel_data_form.Option(link_id, link_id) for link_id in all_links
- ]
- wokkel_form.addField(
- wokkel_data_form.Field(
- "list-single",
- "link_to_delete",
+ link_options = [Option(value=link_id, label=link_id) for link_id in all_links]
+ form.fields.append(
+ ListSingle(
+ var="link_to_delete",
label=D_("Select Link to Delete"),
options=link_options,
required=True,
)
)
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
- form = DataForm.from_wokkel_form(wokkel_form)
- notes = []
-
- elif len(actions) == 2:
- try:
- x_elt = next(command_elt.elements(wokkel_data_form.NS_X_DATA, "x"))
- answer_form = wokkel_data_form.Form.fromElement(x_elt)
- link_to_delete = answer_form["link_to_delete"]
-
- await self.ad_hoc_registration_data.adel(link_to_delete)
-
- status = Status.COMPLETED
- form = None
- notes = [
+ else:
+ link_to_delete = page_data.form.get_field("link_to_delete", ListSingle).value
+ if not link_to_delete:
+ raise AdHocError(Error.BAD_PAYLOAD)
+ await self.ad_hoc_registration_data.adel(link_to_delete)
+ return AdHocCallbackData(
+ status=Status.COMPLETED,
+ notes=[
Note(
- type=NoteType.INFO,
text=D_(
"Registration link {link_id} deleted successfully."
).format(link_id=link_to_delete),
)
- ]
- except (KeyError, StopIteration, ValueError):
- raise AdHocError(Error.BAD_PAYLOAD)
- else:
- raise AdHocError(Error.INTERNAL)
-
- return AdHocCallbackData(form=form, status=status, notes=notes)
+ ],
+ )
diff --git a/libervia/backend/plugins/plugin_comp_ap_gateway/ad_hoc.py b/libervia/backend/plugins/plugin_comp_ap_gateway/ad_hoc.py
index 59020f3d..a2c51f6c 100644
--- a/libervia/backend/plugins/plugin_comp_ap_gateway/ad_hoc.py
+++ b/libervia/backend/plugins/plugin_comp_ap_gateway/ad_hoc.py
@@ -18,20 +18,22 @@
from typing import cast
from twisted.words.protocols.jabber import jid
-from twisted.words.xish import domish
from wokkel import data_form
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.i18n import D_, _
from libervia.backend.core.log import getLogger
-from libervia.backend.memory.memory import SessionData
-from libervia.backend.plugins.plugin_xep_0004 import DataForm
+from libervia.backend.plugins.plugin_xep_0004 import DataForm, Text
from libervia.backend.plugins.plugin_xep_0050 import (
XEP_0050,
AdHocCallbackData,
+ AdHocCommand,
+ AdHocError,
+ Error,
Note,
NoteType,
+ PageData,
Status,
)
@@ -49,59 +51,36 @@ class APAdHocService:
self._c = cast(XEP_0050, self.host.plugins["XEP-0050"])
def init(self, client: SatXMPPEntity) -> None:
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self.xmpp_jid_node_2_ap_actor,
- "Convert XMPP JID/Node to AP actor",
- node=NS_XMPP_JID_NODE_2_AP,
- allowed_magics=[C.ENTITY_ALL],
+ AdHocCommand(
+ callback=self.xmpp_jid_node_2_ap_actor,
+ label=D_("Convert XMPP JID/Node to AP actor"),
+ node=NS_XMPP_JID_NODE_2_AP,
+ allowed_magics=[C.ENTITY_ALL],
+ form=DataForm(
+ title=D_("XMPP JID/node to AP actor conversion"),
+ namespace=NS_XMPP_JID_NODE_2_AP,
+ fields=[Text(var="jid", required=True), Text(var="node")],
+ ),
+ ),
)
async def xmpp_jid_node_2_ap_actor(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Convert XMPP JID/Node to AP actor.
- @param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: When the JID is invalid or when no actor is found.
"""
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- command_form = data_form.Form.fromElement(x_elt)
- except StopIteration:
- command_form = None
- if command_form is None or len(command_form.fields) == 0:
- # root request
- status = Status.EXECUTING
- wokkel_form = data_form.Form(
- "form",
- title="XMPP JID/node to AP actor conversion",
- formNamespace=NS_XMPP_JID_NODE_2_AP,
- )
-
- field = data_form.Field("text-single", "jid", required=True)
- wokkel_form.addField(field)
-
- field = data_form.Field("text-single", "node", required=False)
- wokkel_form.addField(field)
-
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=[]
- )
- else:
- 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)
- return AdHocCallbackData(
- status=Status.COMPLETED, notes=[Note(type=NoteType.INFO, text=actor)]
- )
+ jid_s = page_data.form.get_field("jid", Text).value
+ if not jid_s:
+ raise AdHocError(Error.BAD_PAYLOAD, text=f'Invalid "jid": {jid_s!r}.')
+ xmpp_jid = jid.JID(jid_s)
+ xmpp_node = page_data.form.get_field("node", Text).value
+ actor = await self.apg.get_ap_account_from_jid_and_node(xmpp_jid, xmpp_node)
+ return AdHocCallbackData(status=Status.COMPLETED, notes=[Note(text=actor)])
diff --git a/libervia/backend/plugins/plugin_comp_email_gateway/ad_hoc.py b/libervia/backend/plugins/plugin_comp_email_gateway/ad_hoc.py
index e23fb38c..e448d30a 100644
--- a/libervia/backend/plugins/plugin_comp_email_gateway/ad_hoc.py
+++ b/libervia/backend/plugins/plugin_comp_email_gateway/ad_hoc.py
@@ -19,14 +19,11 @@
from typing import TYPE_CHECKING, NamedTuple, cast
from twisted.mail.imap4 import IMAP4Exception
-from twisted.words.protocols.jabber import jid
-from twisted.words.xish import domish
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 SessionData
from libervia.backend.plugins.plugin_xep_0004 import (
Boolean,
DataForm,
@@ -41,6 +38,7 @@ from libervia.backend.plugins.plugin_xep_0050 import (
Error,
Note,
NoteType,
+ PageData,
Status,
XEP_0050,
)
@@ -55,19 +53,26 @@ if TYPE_CHECKING:
class ImapInfo(NamedTuple):
client: "IMAPClient"
- uid: str
- form: DataForm
+ uid: int
class EmailAdHocService:
"""Ad-Hoc commands for AP Gateway"""
def __init__(self, email_gw: "EmailGatewayComponent"):
+ """Initialize the EmailAdHocService.
+
+ @param email_gw: The EmailGatewayComponent instance.
+ """
self.host = email_gw.host
self.email_gw = email_gw
self._c = cast(XEP_0050, self.host.plugins["XEP-0050"])
def init(self, client: SatXMPPEntity) -> None:
+ """Initialize ad-hoc commands for email gateway.
+
+ @param client: Libervia client instance.
+ """
self._c.register_ad_hoc_command(
client,
AdHocCommand(
@@ -121,17 +126,19 @@ class EmailAdHocService:
),
)
- async def get_uid(self, command_elt: domish.Element) -> ImapInfo:
- iq_elt = command_elt.parent
- assert iq_elt is not None
- requestor = jid.JID(iq_elt["from"]).userhostJID()
- form = DataForm.from_element(command_elt)
- data = form.get_values()
+ async def get_uid(self, page_data: PageData) -> ImapInfo:
+ """Get IMAP UID for a given message ID.
+
+ @param page_data: Ad-Hoc page data containing the message ID.
+ @return: ImapInfo instance, with UID and client.
+ """
try:
- message_id = cast(str, data["uid"][0])
- except (KeyError, IndexError):
- log.warning('"uid" field is mandatory.')
- raise AdHocError(Error.BAD_PAYLOAD)
+ message_id = page_data.form.get_field("uid", Text).value
+ if message_id is None:
+ raise ValueError('"uid" must be set.')
+ except Exception as e:
+ raise AdHocError(Error.BAD_PAYLOAD, text=f'"uid" field is mandatory. {e}')
+ requestor = page_data.requestor
try:
user_data = self.email_gw.users_data[requestor]
except KeyError:
@@ -146,32 +153,24 @@ class EmailAdHocService:
raise AdHocError(
Error.ITEM_NOT_FOUND, f"The message {message_id!r} is not found."
)
- return ImapInfo(user_data.imap_client, uid, form)
+ return ImapInfo(user_data.imap_client, uid)
async def delete_email(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Delete an email from IMAP server.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
"""
- imap_info = await self.get_uid(command_elt)
- values = imap_info.form.get_values()
- try:
- confirm_str = cast(str, values["confirm"][0] or C.BOOL_FALSE)
- except IndexError:
- confirm_str = C.BOOL_FALSE
- if not C.bool(confirm_str):
+ imap_info = await self.get_uid(page_data)
+ confirm = page_data.form.get_field("confirm", Boolean).value
+ if confirm is None:
+ raise AdHocError(Error.BAD_PAYLOAD, text='"confirm" must be set.')
+ if not confirm:
return AdHocCallbackData(
status=Status.CANCELED,
notes=[Note(text=f"User didn't confirm deletion.")],
@@ -188,27 +187,17 @@ class EmailAdHocService:
async def move_email(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Move an email to another IMAP mailbox.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
"""
- imap_info = await self.get_uid(command_elt)
- values = imap_info.form.get_values()
- try:
- dest_mailbox = cast(str, values["dest"][0]).strip()
- if not dest_mailbox:
- raise IndexError
- except IndexError:
+ imap_info = await self.get_uid(page_data)
+ dest_mailbox = page_data.form.get_field("dest", Text).value
+ if dest_mailbox is None or not (dest_mailbox := dest_mailbox.strip()):
raise AdHocError(
Error.BAD_PAYLOAD, D_("You must indicate a destination mailbox.")
)
@@ -240,26 +229,18 @@ class EmailAdHocService:
async def change_flags(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: str,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Change flags of an email.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
"""
- try:
- imap_info = session_data["imap_info"]
- except KeyError:
+ if page_data.idx == 1:
# We are on the second page of the commands, were user select flags.
- imap_info = await self.get_uid(command_elt)
- session_data["imap_info"] = imap_info
+ imap_info = await self.get_uid(page_data)
+ page_data.session_data["imap_info"] = imap_info
# FIXME: We only support INBOX for now.
mailbox = "INBOX"
mailbox_data = imap_info.client.mailboxes_data.get(mailbox, {})
@@ -276,9 +257,12 @@ class EmailAdHocService:
)
flags_data = await imap_info.client.fetchFlags(imap_info.uid, uid=True)
- try:
- current_flags = flags_data[imap_info.uid]["FLAGS"]
- except KeyError:
+ str_uid = str(imap_info.uid)
+ for flags_value in flags_data.values():
+ if flags_value["UID"] == str_uid:
+ current_flags = flags_value["FLAGS"]
+ break
+ else:
return AdHocCallbackData(
status=Status.CANCELED,
notes=[
@@ -301,12 +285,10 @@ class EmailAdHocService:
)
return AdHocCallbackData(form=form, status=Status.EXECUTING)
else:
- form = DataForm.from_element(command_elt)
- data = form.get_values()
- try:
- new_flags = data["flags"]
- except KeyError:
- raise AdHocError(Error.BAD_PAYLOAD)
+ # This is the third and last page of the command, where flags are actually
+ # set.
+ imap_info = page_data.session_data["imap_info"]
+ new_flags = page_data.form.get_field("flags", ListMulti).values
await imap_info.client.setFlags(imap_info.uid, flags=new_flags, uid=True)
return AdHocCallbackData(
status=Status.COMPLETED, notes=[Note(text=D_("Flags set successfully."))]
diff --git a/libervia/backend/plugins/plugin_comp_file_sharing_management.py b/libervia/backend/plugins/plugin_comp_file_sharing_management.py
index b43eaaf9..4c158ec3 100644
--- a/libervia/backend/plugins/plugin_comp_file_sharing_management.py
+++ b/libervia/backend/plugins/plugin_comp_file_sharing_management.py
@@ -19,8 +19,6 @@
import os.path
from functools import partial
from typing import cast
-from twisted.words.xish import domish
-from wokkel import data_form
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from libervia.backend.core.core_types import SatXMPPEntity
@@ -28,15 +26,14 @@ 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.memory.memory import SessionData
-from libervia.backend.plugins.plugin_xep_0004 import DataForm, Text
+from libervia.backend.plugins.plugin_xep_0004 import Boolean, DataForm, Text
from libervia.backend.plugins.plugin_xep_0050 import (
XEP_0050,
- Action,
AdHocCallbackData,
+ AdHocCommand,
AdHocError,
Note,
- NoteType,
+ PageData,
Status,
Error,
)
@@ -69,18 +66,18 @@ NS_FILE_MANAGEMENT_THUMB = "https://salut-a-toi.org/protocol/file-management:0#t
NS_FILE_MANAGEMENT_QUOTA = "https://salut-a-toi.org/protocol/file-management:0#quota"
-class WorkflowError(Exception):
- """Raised when workflow can't be completed"""
-
- def __init__(self, err_args):
- """
- @param err_args(tuple): arguments to return to finish the command workflow
- """
- Exception.__init__(self)
- self.err_args = err_args
+ROOT_FORM = DataForm(
+ type="form",
+ title="File Management",
+ namespace=NS_FILE_MANAGEMENT,
+ fields=[
+ Text(var="path", required=True),
+ Text(var="namespace", required=True),
+ ],
+)
-class FileSharingManagement(object):
+class FileSharingManagement:
# This is a temporary way (Q&D) to handle stored files, a better way (using pubsub
# syntax?) should be elaborated and proposed as a standard.
@@ -100,33 +97,44 @@ class FileSharingManagement(object):
)
def profile_connected(self, client):
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self._on_change_file,
- "Change Permissions of File(s)",
- node=NS_FILE_MANAGEMENT_PERM,
- allowed_magics=[C.ENTITY_ALL],
+ AdHocCommand(
+ callback=self._on_change_file,
+ label=D_("Change Permissions of File(s)"),
+ node=NS_FILE_MANAGEMENT_PERM,
+ allowed_magics=[C.ENTITY_ALL],
+ form=ROOT_FORM,
+ ),
)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self._on_delete_file,
- "Delete File(s)",
- node=NS_FILE_MANAGEMENT_DELETE,
- allowed_magics=[C.ENTITY_ALL],
+ AdHocCommand(
+ callback=self._on_delete_file,
+ label=D_("Delete File(s)"),
+ node=NS_FILE_MANAGEMENT_DELETE,
+ allowed_magics=[C.ENTITY_ALL],
+ form=ROOT_FORM,
+ ),
)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self._on_gen_thumbnails,
- "Generate Thumbnails",
- node=NS_FILE_MANAGEMENT_THUMB,
- allowed_magics=[C.ENTITY_ALL],
+ AdHocCommand(
+ callback=self._on_gen_thumbnails,
+ label=D_("Generate Thumbnails"),
+ node=NS_FILE_MANAGEMENT_THUMB,
+ allowed_magics=[C.ENTITY_ALL],
+ form=ROOT_FORM,
+ ),
)
- self._c.add_ad_hoc_command(
+ self._c.register_ad_hoc_command(
client,
- self._on_quota,
- "Get Quota",
- node=NS_FILE_MANAGEMENT_QUOTA,
- allowed_magics=[C.ENTITY_ALL],
+ AdHocCommand(
+ callback=self._on_quota,
+ label=D_("Get Quota"),
+ node=NS_FILE_MANAGEMENT_QUOTA,
+ allowed_magics=[C.ENTITY_ALL],
+ ),
)
def _delete(self, service_jid_s, path, namespace, profile):
@@ -141,56 +149,28 @@ class FileSharingManagement(object):
)
)
- def _err(self, reason):
- """Helper method to get argument to return for error
+ async def _get_file_data(self, client: SatXMPPEntity, page_data: PageData) -> dict:
+ """Retrieve field requested in root form.
- workflow will be interrupted with an error note
- @param reason(unicode): reason of the error
- @return (tuple): value to return
+ "found_file" will also be set in session_data.
+ @param client: SatXMPPEntity
+ @param page_data: Data of the page.
+ @return: found file data
+
+ @raise AdHocError: something is wrong
"""
- status = Status.COMPLETED
- form = None
- note = (NoteType.ERROR, reason)
- return form, status, None, note
-
- def _get_root_args(self) -> tuple[DataForm, Status, None, None]:
- """Create the form to select the file to use
-
- @return (tuple): value to return
- """
- status = Status.EXECUTING
- form = DataForm(
- type="form",
- title="File Management",
- form_namespace=NS_FILE_MANAGEMENT,
- fields=[
- Text(var="path", required=True),
- Text(var="namespace", required=True),
- ],
- )
-
- return form, status, None, None
-
- async def _get_file_data(self, client, session_data, command_form):
- """Retrieve field requested in root form
-
- "found_file" will also be set in session_data
- @param command_form(data_form.Form): response to root form
- @return (D(dict)): found file data
- @raise WorkflowError: something is wrong
- """
- fields = command_form.fields
try:
- path = fields["path"].value.strip()
- namespace = fields["namespace"].value or None
- except KeyError:
- self._c.ad_hoc_error(Error.BAD_PAYLOAD)
+ path = page_data.form.get_field("path", Text).value
+ namespace = page_data.form.get_field("namespace", Text).value
+ except KeyError as e:
+ raise AdHocError(Error.BAD_PAYLOAD) from e
- if not path:
- self._c.ad_hoc_error(Error.BAD_PAYLOAD)
+ if path is None:
+ raise AdHocError(Error.BAD_PAYLOAD)
+
+ if not (path := path.strip()):
+ raise AdHocError(Error.BAD_PAYLOAD)
- requestor = session_data["requestor"]
- requestor_bare = requestor.userhostJID()
path = path.rstrip("/")
parent_path, basename = os.path.split(path)
@@ -200,24 +180,24 @@ class FileSharingManagement(object):
try:
found_files = await self.host.memory.get_files(
client,
- requestor_bare,
+ page_data.requestor,
path=parent_path,
name=basename,
- namespace=namespace,
+ namespace=namespace or None,
)
found_file = found_files[0]
except (exceptions.NotFound, IndexError):
- raise WorkflowError(self._err(_("file not found")))
+ raise AdHocError(Error.ITEM_NOT_FOUND, text=_("file not found"))
except exceptions.PermissionError:
- raise WorkflowError(self._err(_("forbidden")))
+ raise AdHocError(Error.FORBIDDEN)
- if found_file["owner"] != requestor_bare:
+ if found_file["owner"] != page_data.requestor:
# only owner can manage files
log.warning(_("Only owner can manage files"))
- raise WorkflowError(self._err(_("forbidden")))
+ raise AdHocError(Error.FORBIDDEN)
- session_data["found_file"] = found_file
- session_data["namespace"] = namespace
+ page_data.session_data["found_file"] = found_file
+ page_data.session_data["namespace"] = namespace
return found_file
def _update_read_permission(self, access, allowed_jids):
@@ -240,7 +220,7 @@ class FileSharingManagement(object):
"""
assert file_data["type"] == C.FILE_TYPE_DIRECTORY
files_data = await self.host.memory.get_files(
- client, requestor, parent=file_data["id"], namespace=namespace
+ client, requestor, parent=file_data["id"], namespace=namespace or None
)
for file_data in files_data:
@@ -261,44 +241,20 @@ class FileSharingManagement(object):
async def _on_change_file(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Change permissions of a file or directory.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: Something went wrong during the workflow.
"""
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- command_form = data_form.Form.fromElement(x_elt)
- except StopIteration:
- command_form = None
-
- found_file = session_data.get("found_file")
- requestor = session_data["requestor"]
- requestor_bare = requestor.userhostJID()
-
- if command_form is None or len(command_form.fields) == 0:
- # root request
- form, status, __, __ = self._get_root_args()
- return AdHocCallbackData(form=form, status=status, notes=[])
-
- elif found_file is None:
- # file selected, we retrieve it and ask for permissions
- try:
- found_file = await self._get_file_data(client, session_data, command_form)
- except WorkflowError as e:
- form, status, __, note = e.err_args
- notes = [Note(type=note[0], text=note[1])] if note else []
- return AdHocCallbackData(form=form, status=status, notes=notes)
+ form = page_data.form
+ session_data = page_data.session_data
+ if page_data.idx == 1:
+ # We retrieve the file and ask for permissions.
+ found_file = await self._get_file_data(client, page_data)
# management request
if found_file["type"] == C.FILE_TYPE_DIRECTORY:
@@ -306,46 +262,49 @@ class FileSharingManagement(object):
else:
instructions = D_("Please select permissions for this file")
- form = data_form.Form(
- "form",
- title="File Management",
- instructions=[instructions],
- formNamespace=NS_FILE_MANAGEMENT,
+ read_allowed_field = Text(
+ type="text-multi",
+ var="read_allowed",
+ desc=D_(
+ "list of jids allowed to read this file (beside yourself), or "
+ '"PUBLIC" to let a public access'
+ ),
)
- field = data_form.Field(
- "text-multi",
- "read_allowed",
- required=False,
- desc="list of jids allowed to read this file (beside yourself), or "
- '"PUBLIC" to let a public access',
+ form = DataForm(
+ title=D_("File Management"),
+ instructions=instructions,
+ namespace=NS_FILE_MANAGEMENT,
+ fields=[read_allowed_field],
)
read_access = found_file["access"].get(C.ACCESS_PERM_READ, {})
access_type = read_access.get("type", C.ACCESS_TYPE_WHITELIST)
if access_type == C.ACCESS_TYPE_PUBLIC:
- field.values = ["PUBLIC"]
+ read_allowed_field.value = "PUBLIC"
else:
- field.values = read_access.get("jids", [])
- form.addField(field)
+ read_allowed_field.value = "\n".join(read_access.get("jids", []))
if found_file["type"] == C.FILE_TYPE_DIRECTORY:
- field = data_form.Field(
- "boolean",
- "recursive",
- value=False,
- required=False,
- desc="Files under it will be made public to follow this dir "
- "permission (only if they don't have already a permission set).",
+ form.fields.append(
+ Boolean(
+ var="recursive",
+ value=False,
+ desc=D_(
+ "Files under it will be made public to follow this dir"
+ " permission (only if they don't have already a permission"
+ " set)."
+ ),
+ )
)
- form.addField(field)
- status = Status.EXECUTING
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(form), status=status, notes=[]
- )
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
else:
# final phase, we'll do permission change here
+ found_file = session_data["found_file"]
+
try:
- read_allowed = command_form.fields["read_allowed"]
+ read_allowed = form.get_field("read_allowed", Text)
+ if read_allowed.value is None:
+ raise KeyError
except KeyError:
raise AdHocError(Error.BAD_PAYLOAD)
@@ -355,14 +314,13 @@ class FileSharingManagement(object):
allowed_jids = None
else:
try:
- allowed_jids = [
- jid.JID(v.strip()) for v in read_allowed.values if v.strip()
- ]
+ lines = [line.strip() for line in read_allowed.value.splitlines()]
+ allowed_jids = [jid.JID(v) for v in lines if v]
except RuntimeError as e:
log.warning(
_("Can't use read_allowed values: {reason}").format(reason=e)
)
- raise AdHocError(Error.BAD_PAYLOAD)
+ raise AdHocError(Error.BAD_PAYLOAD, text=str(e))
if found_file["type"] == C.FILE_TYPE_FILE:
await self.host.memory.file_update(
@@ -372,9 +330,9 @@ class FileSharingManagement(object):
)
else:
try:
- recursive = command_form.fields["recursive"]
+ recursive = form.get_field("recursive", Boolean)
except KeyError:
- raise AdHocError(Error.BAD_PAYLOAD)
+ recursive = False
await self.host.memory.file_update(
found_file["id"],
"access",
@@ -386,56 +344,32 @@ class FileSharingManagement(object):
# can read them.
namespace = session_data["namespace"]
await self._update_dir(
- client, requestor_bare, namespace, found_file, "PUBLIC"
+ client, page_data.requestor, namespace, found_file, "PUBLIC"
)
# job done, we can end the session
return AdHocCallbackData(
status=Status.COMPLETED,
- notes=[Note(type=NoteType.INFO, text=_("management session done"))],
+ notes=[Note(text=_("management session done"))],
)
async def _on_delete_file(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Delete a file or directory.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: Something went wrong during the workflow.
"""
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- command_form = data_form.Form.fromElement(x_elt)
- except StopIteration:
- command_form = None
+ session_data = page_data.session_data
- found_file = session_data.get("found_file")
- requestor = session_data["requestor"]
- requestor_bare = requestor.userhostJID()
-
- if command_form is None or len(command_form.fields) == 0:
- # root request
- form, status, __, __ = self._get_root_args()
- return AdHocCallbackData(form=form, status=status, notes=[])
-
- elif found_file is None:
- # file selected, we need confirmation before actually deleting
- try:
- found_file = await self._get_file_data(client, session_data, command_form)
- except WorkflowError as e:
- form, status, __, note = e.err_args
- notes = [Note(type=note[0], text=note[1])] if note else []
- return AdHocCallbackData(form=form, status=status, notes=notes)
+ if page_data.idx == 1:
+ found_file = await self._get_file_data(client, page_data)
+ session_data["found_file"] = found_file
if found_file["type"] == C.FILE_TYPE_DIRECTORY:
msg = D_(
"Are you sure to delete directory {name} and all files and "
@@ -445,41 +379,36 @@ class FileSharingManagement(object):
msg = D_(
"Are you sure to delete file {name}?".format(name=found_file["name"])
)
- form = data_form.Form(
- "form",
- title="File Management",
- instructions=[msg],
- formNamespace=NS_FILE_MANAGEMENT,
- )
- field = data_form.Field(
- "boolean",
- "confirm",
- value=False,
- required=True,
- desc="check this box to confirm",
- )
- form.addField(field)
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(form), status=Status.EXECUTING
+ form = DataForm(
+ title=D_("File Management"),
+ instructions=msg,
+ namespace=NS_FILE_MANAGEMENT,
+ fields=[
+ Boolean(
+ var="confirm",
+ value=False,
+ required=True,
+ desc=D_("Check this box to confirm."),
+ )
+ ],
)
+ return AdHocCallbackData(form=form, status=Status.EXECUTING)
else:
# final phase, we'll do deletion here
+ found_file = session_data["found_file"]
try:
- confirmed = C.bool(command_form.fields["confirm"].value)
+ confirmed = page_data.form.get_field("confirm", Boolean).value
except KeyError:
raise AdHocError(Error.BAD_PAYLOAD)
if not confirmed:
- note = None
+ notes = []
else:
recursive = found_file["type"] == C.FILE_TYPE_DIRECTORY
await self.host.memory.file_delete(
- client, requestor_bare, found_file["id"], recursive
+ client, page_data.requestor, found_file["id"], recursive
)
- note = Note(type=NoteType.INFO, text=_("file deleted"))
- status = Status.COMPLETED
- form = None
- notes = [note] if note else []
+ notes = [Note(text=D_("file deleted"))]
return AdHocCallbackData(status=Status.COMPLETED, notes=notes)
def _update_thumbs(self, extra, thumbnails):
@@ -492,7 +421,7 @@ class FileSharingManagement(object):
"""
if file_data["type"] == C.FILE_TYPE_DIRECTORY:
sub_files_data = await self.host.memory.get_files(
- client, requestor, parent=file_data["id"], namespace=namespace
+ client, requestor, parent=file_data["id"], namespace=namespace or None
)
for sub_file_data in sub_files_data:
await self._gen_thumbs(client, requestor, namespace, sub_file_data)
@@ -536,91 +465,57 @@ class FileSharingManagement(object):
async def _on_gen_thumbnails(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Generate thumbnails for a file or directory.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: Something went wrong during the workflow.
"""
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- command_form = data_form.Form.fromElement(x_elt)
- except StopIteration:
- command_form = None
+ found_file = await self._get_file_data(client, page_data)
- found_file = session_data.get("found_file")
- requestor = session_data["requestor"]
+ log.info("Generating thumbnails.")
+ await self._gen_thumbs(
+ client, page_data.requestor, found_file["namespace"], found_file
+ )
- if command_form is None or len(command_form.fields) == 0:
- # root request
- form, status, __, __ = self._get_root_args()
- return AdHocCallbackData(form=form, status=status, notes=[])
-
- elif found_file is None:
- # file selected, we retrieve it and ask for permissions
- try:
- found_file = await self._get_file_data(client, session_data, command_form)
- except WorkflowError as e:
- form, status, __, note = e.err_args
- notes = [Note(type=note[0], text=note[1])] if note else []
- return AdHocCallbackData(form=form, status=status, notes=notes)
-
- log.info("Generating thumbnails as requested")
- await self._gen_thumbs(client, requestor, found_file["namespace"], found_file)
-
- # job done, we can end the session
- return AdHocCallbackData(
- status=Status.COMPLETED,
- notes=[Note(type=NoteType.INFO, text=_("thumbnails generated"))],
- )
- else:
- raise exceptions.InternalError("This code should not be reached.")
+ return AdHocCallbackData(
+ status=Status.COMPLETED, notes=[Note(text=D_("thumbnails generated"))]
+ )
async def _on_quota(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Get current quota information.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: Something went wrong during the workflow.
"""
- requestor = session_data["requestor"]
- quota = self.host.plugins["file_sharing"].get_quota(client, requestor)
+ requestor = page_data.requestor
+ quota = self.host.plugins["file-sharing"].get_quota(client, requestor)
try:
size_used = await self.host.memory.file_get_used_space(client, requestor)
except exceptions.PermissionError:
raise AdHocError(Error.FORBIDDEN)
- status = Status.COMPLETED
- wokkel_form = data_form.Form("result")
- wokkel_form.makeFields({"quota": quota, "user": size_used})
+ form = DataForm(
+ type="result",
+ fields=[
+ Text(var="quota", value=str(quota) if quota is not None else "unlimited"),
+ Text(var="used", value=str(size_used)),
+ ],
+ )
note = Note(
- type=NoteType.INFO,
- text=_("You are currently using {size_used} on {size_quota}").format(
+ text=D_("You are currently using {size_used} on {size_quota}.").format(
size_used=utils.get_human_size(size_used),
size_quota=(
_("unlimited quota") if quota is None else utils.get_human_size(quota)
),
),
)
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=status, notes=[note]
- )
+ return AdHocCallbackData(form=form, status=Status.COMPLETED, notes=[note])
diff --git a/libervia/backend/plugins/plugin_xep_0004.py b/libervia/backend/plugins/plugin_xep_0004.py
index 93d51174..0e528e6a 100644
--- a/libervia/backend/plugins/plugin_xep_0004.py
+++ b/libervia/backend/plugins/plugin_xep_0004.py
@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Self, TYPE_CHECKING
+from typing import Self, TYPE_CHECKING, Type, TypeVar
from typing import Annotated, Literal
from pydantic import BaseModel, Field
@@ -116,12 +116,14 @@ DataField = Annotated[
Field(discriminator="type"),
]
+T = TypeVar("T", bound=DataField)
+
class DataForm(BaseModel):
type: Literal["form", "submit", "result", "cancel"] = "form"
title: str | None = None
instructions: str | None = None
- form_namespace: str | None = None
+ namespace: str | None = None
fields: list[DataField] = Field(default_factory=list)
def get_values(self) -> dict[str, list[None | str | bool | jid.JID | domish.Element]]:
@@ -140,6 +142,35 @@ class DataForm(BaseModel):
if field.var == field_var:
return field
+ def get_field(self, field_var: str, expected_type: Type[T]) -> T:
+ """Retrieve a field with static type verification.
+
+ This method solves linter complaints by providing static type guarantees.
+ Always use this when you know the expected field type.
+ Example::
+
+ text_field = form.get_field("username", Text)
+ username = text_field.value
+
+ @param field_var: Field var to retrieve.
+ @param expected_type: Expected field type (Text, Boolean, etc.).
+ @return: Field instance strictly typed as expected_type.
+ @raise KeyError: If field doesn't exist.
+ @raise TypeError: If field exists but is wrong type.
+
+ """
+ for field in self.fields:
+ if field.var == field_var:
+ if not isinstance(field, expected_type):
+ raise TypeError(
+ f"Field {field_var!r} is {type(field).__name__} "
+ f"(XEP-0004 type: {getattr(field, 'type', 'unknown')}), "
+ f"expected {expected_type.__name__}"
+ )
+ return field
+
+ raise KeyError(f"Field '{field_var}' not found in data form")
+
@classmethod
def from_wokkel_form(
cls, form: data_form.Form, source_form: Self | None = None
@@ -242,7 +273,7 @@ class DataForm(BaseModel):
type=form.formType,
title=form.title,
instructions="\n".join(form.instructions) if form.instructions else None,
- form_namespace=form.formNamespace,
+ namespace=form.formNamespace,
fields=fields,
)
@@ -252,7 +283,7 @@ class DataForm(BaseModel):
formType=self.type,
title=self.title,
instructions=self.instructions.splitlines() if self.instructions else None,
- formNamespace=self.form_namespace,
+ formNamespace=self.namespace,
)
for field in self.fields:
diff --git a/libervia/backend/plugins/plugin_xep_0050.py b/libervia/backend/plugins/plugin_xep_0050.py
index f6669cc9..b4892c71 100644
--- a/libervia/backend/plugins/plugin_xep_0050.py
+++ b/libervia/backend/plugins/plugin_xep_0050.py
@@ -19,10 +19,10 @@
from collections.abc import Callable, Coroutine
from enum import Enum, StrEnum, auto
-from typing import Any, NamedTuple, Self, cast
+from typing import Any, NamedTuple, cast
import importlib
-from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
+from pydantic import BaseModel, Field, ValidationInfo, field_validator
from pydantic import field_serializer
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
@@ -38,8 +38,8 @@ from libervia.backend.core.core_types import SatXMPPClient, SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory.memory import SessionData, Sessions
-from libervia.backend.models.types import DomishElementType, JIDType
-from libervia.backend.plugins.plugin_xep_0004 import DataForm
+from libervia.backend.models.types import JIDType
+from libervia.backend.plugins.plugin_xep_0004 import DataForm, ListSingle, Option
from libervia.backend.tools import utils, xml_tools
from libervia.backend.tools.common import data_format
@@ -162,7 +162,7 @@ class PageData(NamedTuple):
with its sources specified, meaning that field types should be set correctly.
In the rare case when there is no form (may happen for first page if form is
specified in ``AdHocCommand``), an empty form will be used.
- @param requestor: The JID of the entity requesting the command execution.
+ @param requestor: Bare JID of the entity requesting the command execution.
@param session_data: A dictionary persisting for the whole command session that can be
used by the callback function to store any kind of data.
@param action: The action being performed as defined in the Action enum.
@@ -181,16 +181,19 @@ class PageData(NamedTuple):
idx: int
+AdHocCallback = Callable[
+ [SatXMPPEntity, PageData],
+ Coroutine[None, None, AdHocCallbackData] | AdHocCallbackData,
+]
+
+
class AdHocCommand(BaseModel):
"""Pydantic model for ad-hoc command parameters.
This model represents the parameters used to create an AdHocCommand instance.
"""
- callback: Callable[
- [SatXMPPEntity, PageData],
- Coroutine[None, None, AdHocCallbackData] | AdHocCallbackData,
- ] = Field(
+ callback: AdHocCallback = Field(
description=(
"The callback function to execute when command is invoked. The "
'import string "path.to.module.function_to_call" can be used.\n\n'
@@ -516,7 +519,7 @@ class AdHocCommandHandler(XMPPHandler):
"""Handle an incoming command request.
@param command_elt: The command element from the request
- @param requestor: JID of the entity making the request
+ @param requestor: Full 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
@@ -541,6 +544,21 @@ class AdHocCommandHandler(XMPPHandler):
if action == Action.PREV:
if session_data_private["page_idx"] > 0:
session_data_private["page_idx"] -= 1
+ if (
+ session_data_private["page_idx"] == 0
+ and self.command.form is not None
+ ):
+ # we're back on the first page with original form, we send id
+ # again.
+ await self._send_answer(
+ AdHocCallbackData(
+ form=self.command.form, status=Status.EXECUTING
+ ),
+ session_id,
+ session_data_private,
+ iq_elt,
+ )
+ return
else:
session_data_private["page_idx"] += 1
page_idx = session_data_private["page_idx"]
@@ -554,7 +572,7 @@ class AdHocCommandHandler(XMPPHandler):
session_data_private["requestor"] = requestor
page_idx = session_data_private["page_idx"] = 0
# Maps page_idx to corresponding form.
- forms = session_data_private["forms"]
+ forms = session_data_private["forms"] = {}
if action != Action.EXECUTE:
log.warning(
"Unexpected action on first page request of ad-hoc command: "
@@ -588,7 +606,7 @@ class AdHocCommandHandler(XMPPHandler):
page_data = PageData(
iq_elt=iq_elt,
form=form,
- requestor=requestor,
+ requestor=requestor.userhostJID(),
session_data=session_data_public,
action=action,
node=self.command.node,
@@ -603,9 +621,11 @@ class AdHocCommandHandler(XMPPHandler):
e.callback_error, session_id, iq_elt, text=e.text, lang=e.lang
)
return
- except Exception:
+ except Exception as e:
log.exception(f"Unexpected error while handling request.")
- await self._send_error(Error.INTERNAL, session_id, iq_elt)
+ await self._send_error(
+ Error.INTERNAL, session_id, iq_elt, text=f"Internal Error: {e}"
+ )
return
await self._send_answer(ret_data, session_id, session_data_private, iq_elt)
@@ -659,7 +679,26 @@ class XEP_0050:
# map from node to AdHocCommandHandler instance
client._XEP_0050_commands = {}
if not client.is_component:
- self.add_ad_hoc_command(client, self._status_callback, _("Status"))
+ self.register_ad_hoc_command(
+ client,
+ AdHocCommand(
+ callback=self._status_callback,
+ label=D_("Status"),
+ form=DataForm(
+ title=D_("Status Selection"),
+ fields=[
+ ListSingle(
+ var="show",
+ options=[
+ Option(value=value, label=label)
+ for value, label in list(SHOWS.items())
+ ],
+ required=True,
+ )
+ ],
+ ),
+ ),
+ )
def do(
self,
@@ -934,61 +973,27 @@ class XEP_0050:
def _status_callback(
self,
client: SatXMPPEntity,
- command_elt: domish.Element,
- session_data: SessionData,
- action: Action,
- node: str,
+ page_data: PageData,
) -> AdHocCallbackData:
"""Ad-hoc command used to change the "show" part of status.
@param client: Libervia client instance.
- @param command_elt: The command element from the request.
- @param session_data: Session data for this command execution.
- @param action: The action being performed (EXECUTE, CANCEL, etc.)
- @param node: The command node identifier.
+ @param page_data: Ad-Hoc page data.
@return: Command results.
@raise AdHocError: Something went wrong.
"""
- actions = session_data.setdefault("actions", [])
- actions.append(action)
-
- if len(actions) == 1:
- # it's our first request, we ask the desired new status
- wokkel_form = data_form.Form("form", title=_("status selection"))
- show_options = [
- data_form.Option(name, label) for name, label in list(SHOWS.items())
- ]
- field = data_form.Field(
- "list-single", "show", options=show_options, required=True
- )
- wokkel_form.addField(field)
-
- return AdHocCallbackData(
- form=DataForm.from_wokkel_form(wokkel_form), status=Status.EXECUTING
- )
-
- elif len(actions) == 2:
- # we should have the answer here
- try:
- x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
- answer_form = data_form.Form.fromElement(x_elt)
- show = answer_form["show"]
- except (KeyError, StopIteration):
- self.ad_hoc_error(Error.BAD_PAYLOAD)
- if show not in SHOWS:
- 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
- return AdHocCallbackData(
- status=Status.COMPLETED, notes=[Note(text=_("Status updated"))]
- )
+ show = page_data.form.get_field("show", ListSingle).value
+ if show not in SHOWS:
+ raise AdHocError(Error.BAD_PAYLOAD, text=f"Invalid value: {show=}")
+ if show == "disconnect":
+ self.host.disconnect(client.profile)
else:
- raise AdHocError(Error.INTERNAL)
+ self.host.presence_set(show=show, profile_key=client.profile)
+
+ return AdHocCallbackData(
+ status=Status.COMPLETED, notes=[Note(text=_("Status updated"))]
+ )
def _run(self, service_jid_s="", node="", profile_key=C.PROF_KEY_NONE):
client = self.host.get_client(profile_key)
@@ -1139,7 +1144,7 @@ class XEP_0050:
def add_ad_hoc_command(
self,
client: SatXMPPEntity,
- callback: Callable,
+ callback: AdHocCallback,
label: str,
node: str | None = None,
features: list[str] | None = None,
diff --git a/libervia/backend/plugins/plugin_xep_0264.py b/libervia/backend/plugins/plugin_xep_0264.py
index 477b42ba..87cf3fc6 100644
--- a/libervia/backend/plugins/plugin_xep_0264.py
+++ b/libervia/backend/plugins/plugin_xep_0264.py
@@ -17,13 +17,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from typing import Literal
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from libervia.backend.core.i18n import _
from libervia.backend.core.constants import Const as C
from libervia.backend.core.log import getLogger
log = getLogger(__name__)
-from twisted.internet import threads
-from twisted.python.failure import Failure
+from twisted.internet import defer, threads
from zope.interface import implementer
@@ -44,11 +45,6 @@ from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
-try:
- from twisted.words.protocols.xmlstream import XMPPHandler
-except ImportError:
- from wokkel.subprotocols import XMPPHandler
-
MIME_TYPE = "image/jpeg"
SAVE_FORMAT = "JPEG" # (cf. Pillow documentation)
@@ -78,22 +74,22 @@ class XEP_0264(object):
# TODO: use an other mechanism than BoB for bigger files
SIZES = (SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG)
- def __init__(self, host):
+ def __init__(self, host) -> None:
log.info(_("Plugin XEP_0264 initialization"))
self.host = host
host.trigger.add("XEP-0234_buildFileElement", self._add_file_thumbnails)
host.trigger.add("XEP-0234_parseFileElement", self._get_file_thumbnails)
- def get_handler(self, client):
+ def get_handler(self, client) -> "XEP_0264_handler":
return XEP_0264_handler()
##Â triggers ##
- def _add_file_thumbnails(self, client, file_elt, extra_args):
+ def _add_file_thumbnails(self, client, file_elt, extra_args) -> Literal[True]:
try:
thumbnails = extra_args["extra"][C.KEY_THUMBNAILS]
except KeyError:
- return
+ return True
for thumbnail in thumbnails:
thumbnail_elt = file_elt.addElement((NS_THUMBS, "thumbnail"))
thumbnail_elt["uri"] = "cid:" + thumbnail["id"]
@@ -103,7 +99,7 @@ class XEP_0264(object):
thumbnail_elt["height"] = str(height)
return True
- def _get_file_thumbnails(self, client, file_elt, file_data):
+ def _get_file_thumbnails(self, client, file_elt, file_data) -> Literal[True]:
thumbnails = []
for thumbnail_elt in file_elt.elements(NS_THUMBS, "thumbnail"):
uri = thumbnail_elt["uri"]
@@ -141,19 +137,26 @@ class XEP_0264(object):
return hashlib.sha256(repr((image_uid, size)).encode()).hexdigest()
def _blocking_gen_thumb(
- self, source_path, size=None, max_age=None, image_uid=None, fix_orientation=True
- ):
+ self,
+ source_path: str,
+ size: tuple[int, int] | None = None,
+ max_age: int | None = None,
+ image_uid: str | None = None,
+ fix_orientation: bool = True,
+ ) -> tuple[tuple[int, int], str]:
"""Generate a thumbnail for image
This is a blocking method and must be executed in a thread
params are the same as for [generate_thumbnail]
+
+ @raise exception.DataError: Can't generate thumbnail.
"""
if size is None:
size = self.SIZE_SMALL
try:
img = Image.open(source_path)
- except IOError:
- return Failure(exceptions.DataError("Can't open image"))
+ except IOError as e:
+ raise exceptions.DataError("Can't open image") from e
img.thumbnail(size)
if fix_orientation:
@@ -170,25 +173,30 @@ class XEP_0264(object):
return img.size, uid
- def generate_thumbnail(
- self, source_path, size=None, max_age=None, image_uid=None, fix_orientation=True
- ):
+ async def generate_thumbnail(
+ self,
+ source_path: str,
+ size: int | None = None,
+ max_age: int | None = None,
+ image_uid: str | None = None,
+ fix_orientation: bool = True,
+ ) -> tuple[tuple[int, int], str]:
"""Generate a thumbnail of image
- @param source_path(unicode): absolute path to source image
- @param size(int, None): max size of the thumbnail
+ @param source_path: absolute path to source image
+ @param size: max size of the thumbnail
can be one of self.SIZE_*
None to use default value (i.e. self.SIZE_SMALL)
- @param max_age(int, None): same as for [memory.cache.Cache.cache_data])
- @param image_uid(unicode, None): unique ID to identify the image
+ @param max_age: same as for [memory.cache.Cache.cache_data]
+ @param image_uid: unique ID to identify the image
use hash whenever possible
if None, source_path will be used
- @param fix_orientation(bool): if True, fix orientation using EXIF data
- @return D(tuple[tuple[int,int], unicode]): tuple with:
+ @param fix_orientation: if True, fix orientation using EXIF data
+ @return: tuple with:
- size of the thumbnail
- unique Id of the thumbnail
"""
- d = threads.deferToThread(
+ return await threads.deferToThread(
self._blocking_gen_thumb,
source_path,
size,
@@ -196,10 +204,6 @@ class XEP_0264(object):
image_uid=image_uid,
fix_orientation=fix_orientation,
)
- d.addErrback(
- lambda failure_: log.error("thumbnail generation error: {}".format(failure_))
- )
- return d
@implementer(iwokkel.IDisco)