plugin XEP-0050: Some more refactoring:
- Arguments are now grouped in a new `PageData` NamedTuple, making it simpler to use, and easier to extend. - Page index is automatically calculated, and can be checked in callback to know which page to show. - Form send by calling entity is automatically parsed, using original form to instantiate the right field types. - A new `DataForm.get_field` method use Generic type as second argument to retrieve the right field when getting a `submit` form. - Updated all classes registering Ad-Hoc commands to use the new system and simplify the code.
This commit is contained in:
parent
e4e879d871
commit
1b3593d95b
9 changed files with 625 additions and 863 deletions
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
|
|
@ -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."))]
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -16,7 +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 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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
# 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 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue