# HG changeset patch # User Goffi # Date 1685707958 -7200 # Node ID 26b7ed2817da509b73cac9279272023efdca4e54 # Parent 7c5654c54fed116e35568db5d79123c99aeb19c6 refactoring: rename `sat_frontends` to `libervia.frontends` diff -r 7c5654c54fed -r 26b7ed2817da libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py --- a/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py Fri Jun 02 12:59:21 2023 +0200 +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py Fri Jun 02 14:12:38 2023 +0200 @@ -25,7 +25,7 @@ from twisted.internet.error import ConnectionRefusedError, ConnectError from libervia.backend.core import exceptions from libervia.backend.tools import config -from sat_frontends.bridge.bridge_frontend import BridgeException +from libervia.frontends.bridge.bridge_frontend import BridgeException log = getLogger(__name__) diff -r 7c5654c54fed -r 26b7ed2817da libervia/backend/plugins/plugin_misc_tarot.py --- a/libervia/backend/plugins/plugin_misc_tarot.py Fri Jun 02 12:59:21 2023 +0200 +++ b/libervia/backend/plugins/plugin_misc_tarot.py Fri Jun 02 14:12:38 2023 +0200 @@ -29,7 +29,7 @@ from libervia.backend.memory import memory from libervia.backend.tools import xml_tools -from sat_frontends.tools.games import TarotCard +from libervia.frontends.tools.games import TarotCard import random diff -r 7c5654c54fed -r 26b7ed2817da libervia/backend/plugins/plugin_misc_text_syntaxes.py --- a/libervia/backend/plugins/plugin_misc_text_syntaxes.py Fri Jun 02 12:59:21 2023 +0200 +++ b/libervia/backend/plugins/plugin_misc_text_syntaxes.py Fri Jun 02 14:12:38 2023 +0200 @@ -203,7 +203,7 @@ TextSyntaxes.OPT_NO_THREAD, ) # TODO: text => XHTML should add to url like in frontends - # it's probably best to move sat_frontends.tools.strings to sat.tools.common or similar + # it's probably best to move libervia.frontends.tools.strings to sat.tools.common or similar self.add_syntax( self.SYNTAX_TEXT, lambda text: escape(text), diff -r 7c5654c54fed -r 26b7ed2817da libervia/backend/tools/common/template_xmlui.py --- a/libervia/backend/tools/common/template_xmlui.py Fri Jun 02 12:59:21 2023 +0200 +++ b/libervia/backend/tools/common/template_xmlui.py Fri Jun 02 14:12:38 2023 +0200 @@ -24,8 +24,8 @@ from functools import partial from libervia.backend.core.log import getLogger -from sat_frontends.tools import xmlui -from sat_frontends.tools import jid +from libervia.frontends.tools import xmlui +from libervia.frontends.tools import jid try: from jinja2 import Markup as safe except ImportError: diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/bridge/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/bridge/bridge_frontend.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/bridge/bridge_frontend.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + + +# SAT communication bridge +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class BridgeException(Exception): + """An exception which has been raised from the backend and arrived to the frontend.""" + + def __init__(self, name, message="", condition=""): + """ + + @param name (str): full exception class name (with module) + @param message (str): error message + @param condition (str) : error condition + """ + super().__init__() + self.fullname = str(name) + self.message = str(message) + self.condition = str(condition) if condition else "" + self.module, __, self.classname = str(self.fullname).rpartition(".") + + def __str__(self): + return self.classname + (f": {self.message}" if self.message else "") + + def __eq__(self, other): + return self.classname == other diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/bridge/dbus_bridge.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/bridge/dbus_bridge.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1512 @@ +#!/usr/bin/env python3 + +# SàT communication bridge +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +import dbus +import ast +from libervia.backend.core.i18n import _ +from libervia.backend.tools import config +from libervia.backend.core.log import getLogger +from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError +from dbus.mainloop.glib import DBusGMainLoop +from .bridge_frontend import BridgeException + + +DBusGMainLoop(set_as_default=True) +log = getLogger(__name__) + + +# Interface prefix +const_INT_PREFIX = config.config_get( + config.parse_main_conf(), + "", + "bridge_dbus_int_prefix", + "org.libervia.Libervia") +const_ERROR_PREFIX = const_INT_PREFIX + ".error" +const_OBJ_PATH = '/org/libervia/Libervia/bridge' +const_CORE_SUFFIX = ".core" +const_PLUGIN_SUFFIX = ".plugin" +const_TIMEOUT = 120 + + +def dbus_to_bridge_exception(dbus_e): + """Convert a DBusException to a BridgeException. + + @param dbus_e (DBusException) + @return: BridgeException + """ + full_name = dbus_e.get_dbus_name() + if full_name.startswith(const_ERROR_PREFIX): + name = dbus_e.get_dbus_name()[len(const_ERROR_PREFIX) + 1:] + else: + name = full_name + # XXX: dbus_e.args doesn't contain the original DBusException args, but we + # receive its serialized form in dbus_e.args[0]. From that we can rebuild + # the original arguments list thanks to ast.literal_eval (secure eval). + message = dbus_e.get_dbus_message() # similar to dbus_e.args[0] + try: + message, condition = ast.literal_eval(message) + except (SyntaxError, ValueError, TypeError): + condition = '' + return BridgeException(name, message, condition) + + +class bridge: + + def bridge_connect(self, callback, errback): + try: + self.sessions_bus = dbus.SessionBus() + self.db_object = self.sessions_bus.get_object(const_INT_PREFIX, + const_OBJ_PATH) + self.db_core_iface = dbus.Interface(self.db_object, + dbus_interface=const_INT_PREFIX + const_CORE_SUFFIX) + self.db_plugin_iface = dbus.Interface(self.db_object, + dbus_interface=const_INT_PREFIX + const_PLUGIN_SUFFIX) + except dbus.exceptions.DBusException as e: + if e._dbus_error_name in ('org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Spawn.ExecFailed'): + errback(BridgeExceptionNoService()) + elif e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': + log.error(_("D-Bus is not launched, please see README to see instructions on how to launch it")) + errback(BridgeInitError) + else: + errback(e) + else: + callback() + #props = self.db_core_iface.getProperties() + + def register_signal(self, functionName, handler, iface="core"): + if iface == "core": + self.db_core_iface.connect_to_signal(functionName, handler) + elif iface == "plugin": + self.db_plugin_iface.connect_to_signal(functionName, handler) + else: + log.error(_('Unknown interface')) + + def __getattribute__(self, name): + """ usual __getattribute__ if the method exists, else try to find a plugin method """ + try: + return object.__getattribute__(self, name) + except AttributeError: + # The attribute is not found, we try the plugin proxy to find the requested method + + def get_plugin_method(*args, **kwargs): + # We first check if we have an async call. We detect this in two ways: + # - if we have the 'callback' and 'errback' keyword arguments + # - or if the last two arguments are callable + + async_ = False + args = list(args) + + if kwargs: + if 'callback' in kwargs: + async_ = True + _callback = kwargs.pop('callback') + _errback = kwargs.pop('errback', lambda failure: log.error(str(failure))) + try: + args.append(kwargs.pop('profile')) + except KeyError: + try: + args.append(kwargs.pop('profile_key')) + except KeyError: + pass + # at this point, kwargs should be empty + if kwargs: + log.warning("unexpected keyword arguments, they will be ignored: {}".format(kwargs)) + elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): + async_ = True + _errback = args.pop() + _callback = args.pop() + + method = getattr(self.db_plugin_iface, name) + + if async_: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = _callback + kwargs['error_handler'] = lambda err: _errback(dbus_to_bridge_exception(err)) + + try: + return method(*args, **kwargs) + except ValueError as e: + if e.args[0].startswith("Unable to guess signature"): + # XXX: if frontend is started too soon after backend, the + # inspection misses methods (notably plugin dynamically added + # methods). The following hack works around that by redoing the + # cache of introspected methods signatures. + log.debug("using hack to work around inspection issue") + proxy = self.db_plugin_iface.proxy_object + IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS + proxy._introspect_state = IN_PROGRESS + proxy._Introspect() + return self.db_plugin_iface.get_dbus_method(name)(*args, **kwargs) + raise e + + return get_plugin_method + + def action_launch(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.action_launch(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def actions_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.actions_get(profile_key, **kwargs) + + def config_get(self, section, name, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.config_get(section, name, **kwargs)) + + def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.connect(profile_key, password, options, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def contact_add(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.contact_add(entity_jid, profile_key, **kwargs) + + def contact_del(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.contact_del(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def contact_get(self, arg_0, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.contact_get(arg_0, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.contact_update(entity_jid, name, groups, profile_key, **kwargs) + + def contacts_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.contacts_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def contacts_get_from_group(self, group, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.contacts_get_from_group(group, profile_key, **kwargs) + + def devices_infos_get(self, bare_jid, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.devices_infos_get(bare_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.disco_find_by_features(namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.disco_infos(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.disco_items(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def encryption_namespace_get(self, arg_0, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.encryption_namespace_get(arg_0, **kwargs)) + + def encryption_plugins_get(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.encryption_plugins_get(**kwargs)) + + def encryption_trust_ui_get(self, to_jid, namespace, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.encryption_trust_ui_get(to_jid, namespace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def entities_data_get(self, jids, keys, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.entities_data_get(jids, keys, profile, **kwargs) + + def entity_data_get(self, jid, keys, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.entity_data_get(jid, keys, profile, **kwargs) + + def features_get(self, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.features_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.history_get(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def image_check(self, arg_0, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.image_check(arg_0, **kwargs)) + + def image_convert(self, source, dest, arg_2, extra, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.image_convert(source, dest, arg_2, extra, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def image_generate_preview(self, image_path, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.image_generate_preview(image_path, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def image_resize(self, image_path, width, height, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.image_resize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def is_connected(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.is_connected(profile_key, **kwargs) + + def main_resource_get(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.main_resource_get(contact_jid, profile_key, **kwargs)) + + def menu_help_get(self, menu_id, language, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.menu_help_get(menu_id, language, **kwargs)) + + def menu_launch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.menu_launch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def menus_get(self, language, security_limit, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.menus_get(language, security_limit, **kwargs) + + def message_encryption_get(self, to_jid, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.message_encryption_get(to_jid, profile_key, **kwargs)) + + def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.message_encryption_start(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def message_encryption_stop(self, to_jid, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.message_encryption_stop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.message_send(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def namespaces_get(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.namespaces_get(**kwargs) + + def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.param_get_a(name, category, attribute, profile_key, **kwargs)) + + def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.param_get_a_async(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.param_set(name, value, category, security_limit, profile_key, **kwargs) + + def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.param_ui_get(security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def params_categories_get(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.params_categories_get(**kwargs) + + def params_register_app(self, xml, security_limit=-1, app='', callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.params_register_app(xml, security_limit, app, **kwargs) + + def params_template_load(self, filename, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.params_template_load(filename, **kwargs) + + def params_template_save(self, filename, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.params_template_save(filename, **kwargs) + + def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.params_values_from_category_get_async(category, security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.presence_set(to_jid, show, statuses, profile_key, **kwargs) + + def presence_statuses_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.presence_statuses_get(profile_key, **kwargs) + + def private_data_delete(self, namespace, key, arg_2, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.private_data_delete(namespace, key, arg_2, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def private_data_get(self, namespace, key, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return str(self.db_core_iface.private_data_get(namespace, key, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) + + def private_data_set(self, namespace, key, data, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.private_data_set(namespace, key, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def profile_create(self, profile, password='', component='', callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.profile_create(profile, password, component, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def profile_delete_async(self, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.profile_delete_async(profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def profile_is_session_started(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.profile_is_session_started(profile_key, **kwargs) + + def profile_name_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.profile_name_get(profile_key, **kwargs)) + + def profile_set_default(self, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.profile_set_default(profile, **kwargs) + + def profile_start_session(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.profile_start_session(password, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def profiles_list_get(self, clients=True, components=False, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.profiles_list_get(clients, components, **kwargs) + + def progress_get(self, id, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.progress_get(id, profile, **kwargs) + + def progress_get_all(self, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.progress_get_all(profile, **kwargs) + + def progress_get_all_metadata(self, profile, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.progress_get_all_metadata(profile, **kwargs) + + def ready_get(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.ready_get(timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def roster_resync(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.roster_resync(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def session_infos_get(self, profile_key, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + return self.db_core_iface.session_infos_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) + + def sub_waiting_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.sub_waiting_get(profile_key, **kwargs) + + def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs) + + def version_get(self, callback=None, errback=None): + if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + return str(self.db_core_iface.version_get(**kwargs)) + + +class AIOBridge(bridge): + + def register_signal(self, functionName, handler, iface="core"): + loop = asyncio.get_running_loop() + async_handler = lambda *args: asyncio.run_coroutine_threadsafe(handler(*args), loop) + return super().register_signal(functionName, async_handler, iface) + + def __getattribute__(self, name): + """ usual __getattribute__ if the method exists, else try to find a plugin method """ + try: + return object.__getattribute__(self, name) + except AttributeError: + # The attribute is not found, we try the plugin proxy to find the requested method + def get_plugin_method(*args, **kwargs): + loop = asyncio.get_running_loop() + fut = loop.create_future() + method = getattr(self.db_plugin_iface, name) + reply_handler = lambda ret=None: loop.call_soon_threadsafe( + fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe( + fut.set_exception, dbus_to_bridge_exception(err)) + try: + method( + *args, + **kwargs, + timeout=const_TIMEOUT, + reply_handler=reply_handler, + error_handler=error_handler + ) + except ValueError as e: + if e.args[0].startswith("Unable to guess signature"): + # same hack as for bridge.__getattribute__ + log.warning("using hack to work around inspection issue") + proxy = self.db_plugin_iface.proxy_object + IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS + proxy._introspect_state = IN_PROGRESS + proxy._Introspect() + self.db_plugin_iface.get_dbus_method(name)( + *args, + **kwargs, + timeout=const_TIMEOUT, + reply_handler=reply_handler, + error_handler=error_handler + ) + + else: + raise e + return fut + + return get_plugin_method + + def bridge_connect(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + super().bridge_connect( + callback=lambda: loop.call_soon_threadsafe(fut.set_result, None), + errback=lambda e: loop.call_soon_threadsafe(fut.set_exception, e) + ) + return fut + + def action_launch(self, callback_id, data, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.action_launch(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def actions_get(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.actions_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def config_get(self, section, name): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.config_get(section, name, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def connect(self, profile_key="@DEFAULT@", password='', options={}): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.connect(profile_key, password, options, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contact_add(self, entity_jid, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contact_add(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contact_del(self, entity_jid, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contact_del(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contact_get(self, arg_0, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contact_get(arg_0, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contact_update(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contacts_get(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contacts_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def contacts_get_from_group(self, group, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.contacts_get_from_group(group, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def devices_infos_get(self, bare_jid, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.devices_infos_get(bare_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.disco_find_by_features(namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.disco_infos(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.disco_items(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def disconnect(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def encryption_namespace_get(self, arg_0): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.encryption_namespace_get(arg_0, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def encryption_plugins_get(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.encryption_plugins_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def encryption_trust_ui_get(self, to_jid, namespace, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.encryption_trust_ui_get(to_jid, namespace, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def entities_data_get(self, jids, keys, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.entities_data_get(jids, keys, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def entity_data_get(self, jid, keys, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.entity_data_get(jid, keys, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def features_get(self, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.features_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.history_get(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def image_check(self, arg_0): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.image_check(arg_0, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def image_convert(self, source, dest, arg_2, extra): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.image_convert(source, dest, arg_2, extra, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def image_generate_preview(self, image_path, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.image_generate_preview(image_path, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def image_resize(self, image_path, width, height): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.image_resize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def is_connected(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.is_connected(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.main_resource_get(contact_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def menu_help_get(self, menu_id, language): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.menu_help_get(menu_id, language, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def menu_launch(self, menu_type, path, data, security_limit, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.menu_launch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def menus_get(self, language, security_limit): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.menus_get(language, security_limit, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def message_encryption_get(self, to_jid, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.message_encryption_get(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.message_encryption_start(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def message_encryption_stop(self, to_jid, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.message_encryption_stop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.message_send(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def namespaces_get(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.namespaces_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.param_get_a(name, category, attribute, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.param_get_a_async(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.param_set(name, value, category, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.param_ui_get(security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def params_categories_get(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.params_categories_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def params_register_app(self, xml, security_limit=-1, app=''): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.params_register_app(xml, security_limit, app, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def params_template_load(self, filename): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.params_template_load(filename, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def params_template_save(self, filename): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.params_template_save(filename, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.params_values_from_category_get_async(category, security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.presence_set(to_jid, show, statuses, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def presence_statuses_get(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.presence_statuses_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def private_data_delete(self, namespace, key, arg_2): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.private_data_delete(namespace, key, arg_2, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def private_data_get(self, namespace, key, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.private_data_get(namespace, key, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def private_data_set(self, namespace, key, data, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.private_data_set(namespace, key, data, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_create(self, profile, password='', component=''): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_create(profile, password, component, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_delete_async(self, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_delete_async(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_is_session_started(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_is_session_started(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_name_get(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_name_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_set_default(self, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_set_default(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profile_start_session(self, password='', profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profile_start_session(password, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def profiles_list_get(self, clients=True, components=False): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.profiles_list_get(clients, components, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def progress_get(self, id, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.progress_get(id, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def progress_get_all(self, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.progress_get_all(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def progress_get_all_metadata(self, profile): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.progress_get_all_metadata(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def ready_get(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.ready_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def roster_resync(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.roster_resync(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def session_infos_get(self, profile_key): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.session_infos_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def sub_waiting_get(self, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.sub_waiting_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.subscription(sub_type, entity, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut + + def version_get(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_core_iface.version_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + return fut diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/bridge/pb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/bridge/pb.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1120 @@ +#!/usr/bin/env python3 + +# SàT communication bridge +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +from logging import getLogger +from functools import partial +from pathlib import Path +from twisted.spread import pb +from twisted.internet import reactor, defer +from twisted.internet.error import ConnectionRefusedError, ConnectError +from libervia.backend.core import exceptions +from libervia.backend.tools import config +from libervia.frontends.bridge.bridge_frontend import BridgeException + +log = getLogger(__name__) + + +class SignalsHandler(pb.Referenceable): + def __getattr__(self, name): + if name.startswith("remote_"): + log.debug("calling an unregistered signal: {name}".format(name=name[7:])) + return lambda *args, **kwargs: None + + else: + raise AttributeError(name) + + def register_signal(self, name, handler, iface="core"): + log.debug("registering signal {name}".format(name=name)) + method_name = "remote_" + name + try: + self.__getattribute__(method_name) + except AttributeError: + pass + else: + raise exceptions.InternalError( + "{name} signal handler has been registered twice".format( + name=method_name + ) + ) + setattr(self, method_name, handler) + + +class bridge(object): + + def __init__(self): + self.signals_handler = SignalsHandler() + + def __getattr__(self, name): + return partial(self.call, name) + + def _generic_errback(self, err): + log.error(f"bridge error: {err}") + + def _errback(self, failure_, ori_errback): + """Convert Failure to BridgeException""" + ori_errback( + BridgeException( + name=failure_.type.decode('utf-8'), + message=str(failure_.value) + ) + ) + + def remote_callback(self, result, callback): + """call callback with argument or None + + if result is not None not argument is used, + else result is used as argument + @param result: remote call result + @param callback(callable): method to call on result + """ + if result is None: + callback() + else: + callback(result) + + def call(self, name, *args, **kwargs): + """call a remote method + + @param name(str): name of the bridge method + @param args(list): arguments + may contain callback and errback as last 2 items + @param kwargs(dict): keyword arguments + may contain callback and errback + """ + callback = errback = None + if kwargs: + try: + callback = kwargs.pop("callback") + except KeyError: + pass + try: + errback = kwargs.pop("errback") + except KeyError: + pass + elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): + errback = args.pop() + callback = args.pop() + d = self.root.callRemote(name, *args, **kwargs) + if callback is not None: + d.addCallback(self.remote_callback, callback) + if errback is not None: + d.addErrback(errback) + + def _init_bridge_eb(self, failure_): + log.error("Can't init bridge: {msg}".format(msg=failure_)) + return failure_ + + def _set_root(self, root): + """set remote root object + + bridge will then be initialised + """ + self.root = root + d = root.callRemote("initBridge", self.signals_handler) + d.addErrback(self._init_bridge_eb) + return d + + def get_root_object_eb(self, failure_): + """Call errback with appropriate bridge error""" + if failure_.check(ConnectionRefusedError, ConnectError): + raise exceptions.BridgeExceptionNoService + else: + raise failure_ + + def bridge_connect(self, callback, errback): + factory = pb.PBClientFactory() + conf = config.parse_main_conf() + get_conf = partial(config.get_conf, conf, "bridge_pb", "") + conn_type = get_conf("connection_type", "unix_socket") + if conn_type == "unix_socket": + local_dir = Path(config.config_get(conf, "", "local_dir")).resolve() + socket_path = local_dir / "bridge_pb" + reactor.connectUNIX(str(socket_path), factory) + elif conn_type == "socket": + host = get_conf("host", "localhost") + port = int(get_conf("port", 8789)) + reactor.connectTCP(host, port, factory) + else: + raise ValueError(f"Unknown pb connection type: {conn_type!r}") + d = factory.getRootObject() + d.addCallback(self._set_root) + if callback is not None: + d.addCallback(lambda __: callback()) + d.addErrback(self.get_root_object_eb) + if errback is not None: + d.addErrback(lambda failure_: errback(failure_.value)) + return d + + def register_signal(self, functionName, handler, iface="core"): + self.signals_handler.register_signal(functionName, handler, iface) + + + def action_launch(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("action_launch", callback_id, data, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def actions_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("actions_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def config_get(self, section, name, callback=None, errback=None): + d = self.root.callRemote("config_get", section, name) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): + d = self.root.callRemote("connect", profile_key, password, options) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contact_add(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contact_add", entity_jid, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contact_del(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contact_del", entity_jid, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contact_get(self, arg_0, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contact_get", arg_0, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contact_update", entity_jid, name, groups, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contacts_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contacts_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def contacts_get_from_group(self, group, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("contacts_get_from_group", group, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def devices_infos_get(self, bare_jid, profile_key, callback=None, errback=None): + d = self.root.callRemote("devices_infos_get", bare_jid, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("disco_find_by_features", namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("disco_infos", entity_jid, node, use_cache, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("disco_items", entity_jid, node, use_cache, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("disconnect", profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def encryption_namespace_get(self, arg_0, callback=None, errback=None): + d = self.root.callRemote("encryption_namespace_get", arg_0) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def encryption_plugins_get(self, callback=None, errback=None): + d = self.root.callRemote("encryption_plugins_get") + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def encryption_trust_ui_get(self, to_jid, namespace, profile_key, callback=None, errback=None): + d = self.root.callRemote("encryption_trust_ui_get", to_jid, namespace, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def entities_data_get(self, jids, keys, profile, callback=None, errback=None): + d = self.root.callRemote("entities_data_get", jids, keys, profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def entity_data_get(self, jid, keys, profile, callback=None, errback=None): + d = self.root.callRemote("entity_data_get", jid, keys, profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def features_get(self, profile_key, callback=None, errback=None): + d = self.root.callRemote("features_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): + d = self.root.callRemote("history_get", from_jid, to_jid, limit, between, filters, profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def image_check(self, arg_0, callback=None, errback=None): + d = self.root.callRemote("image_check", arg_0) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def image_convert(self, source, dest, arg_2, extra, callback=None, errback=None): + d = self.root.callRemote("image_convert", source, dest, arg_2, extra) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def image_generate_preview(self, image_path, profile_key, callback=None, errback=None): + d = self.root.callRemote("image_generate_preview", image_path, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def image_resize(self, image_path, width, height, callback=None, errback=None): + d = self.root.callRemote("image_resize", image_path, width, height) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def is_connected(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("is_connected", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def main_resource_get(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("main_resource_get", contact_jid, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def menu_help_get(self, menu_id, language, callback=None, errback=None): + d = self.root.callRemote("menu_help_get", menu_id, language) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def menu_launch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): + d = self.root.callRemote("menu_launch", menu_type, path, data, security_limit, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def menus_get(self, language, security_limit, callback=None, errback=None): + d = self.root.callRemote("menus_get", language, security_limit) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def message_encryption_get(self, to_jid, profile_key, callback=None, errback=None): + d = self.root.callRemote("message_encryption_get", to_jid, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None): + d = self.root.callRemote("message_encryption_start", to_jid, namespace, replace, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def message_encryption_stop(self, to_jid, profile_key, callback=None, errback=None): + d = self.root.callRemote("message_encryption_stop", to_jid, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): + d = self.root.callRemote("message_send", to_jid, message, subject, mess_type, extra, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def namespaces_get(self, callback=None, errback=None): + d = self.root.callRemote("namespaces_get") + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("param_get_a", name, category, attribute, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("param_get_a_async", name, category, attribute, security_limit, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("param_set", name, value, category, security_limit, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("param_ui_get", security_limit, app, extra, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def params_categories_get(self, callback=None, errback=None): + d = self.root.callRemote("params_categories_get") + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def params_register_app(self, xml, security_limit=-1, app='', callback=None, errback=None): + d = self.root.callRemote("params_register_app", xml, security_limit, app) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def params_template_load(self, filename, callback=None, errback=None): + d = self.root.callRemote("params_template_load", filename) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def params_template_save(self, filename, callback=None, errback=None): + d = self.root.callRemote("params_template_save", filename) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("params_values_from_category_get_async", category, security_limit, app, extra, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("presence_set", to_jid, show, statuses, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def presence_statuses_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("presence_statuses_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def private_data_delete(self, namespace, key, arg_2, callback=None, errback=None): + d = self.root.callRemote("private_data_delete", namespace, key, arg_2) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def private_data_get(self, namespace, key, profile_key, callback=None, errback=None): + d = self.root.callRemote("private_data_get", namespace, key, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def private_data_set(self, namespace, key, data, profile_key, callback=None, errback=None): + d = self.root.callRemote("private_data_set", namespace, key, data, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_create(self, profile, password='', component='', callback=None, errback=None): + d = self.root.callRemote("profile_create", profile, password, component) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_delete_async(self, profile, callback=None, errback=None): + d = self.root.callRemote("profile_delete_async", profile) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_is_session_started(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("profile_is_session_started", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_name_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("profile_name_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_set_default(self, profile, callback=None, errback=None): + d = self.root.callRemote("profile_set_default", profile) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profile_start_session(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("profile_start_session", password, profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def profiles_list_get(self, clients=True, components=False, callback=None, errback=None): + d = self.root.callRemote("profiles_list_get", clients, components) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def progress_get(self, id, profile, callback=None, errback=None): + d = self.root.callRemote("progress_get", id, profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def progress_get_all(self, profile, callback=None, errback=None): + d = self.root.callRemote("progress_get_all", profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def progress_get_all_metadata(self, profile, callback=None, errback=None): + d = self.root.callRemote("progress_get_all_metadata", profile) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def ready_get(self, callback=None, errback=None): + d = self.root.callRemote("ready_get") + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def roster_resync(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("roster_resync", profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def session_infos_get(self, profile_key, callback=None, errback=None): + d = self.root.callRemote("session_infos_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def sub_waiting_get(self, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("sub_waiting_get", profile_key) + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): + d = self.root.callRemote("subscription", sub_type, entity, profile_key) + if callback is not None: + d.addCallback(lambda __: callback()) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + def version_get(self, callback=None, errback=None): + d = self.root.callRemote("version_get") + if callback is not None: + d.addCallback(callback) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback) + + +class AIOSignalsHandler(SignalsHandler): + + def register_signal(self, name, handler, iface="core"): + async_handler = lambda *args, **kwargs: defer.Deferred.fromFuture( + asyncio.ensure_future(handler(*args, **kwargs))) + return super().register_signal(name, async_handler, iface) + + +class AIOBridge(bridge): + + def __init__(self): + self.signals_handler = AIOSignalsHandler() + + def _errback(self, failure_): + """Convert Failure to BridgeException""" + raise BridgeException( + name=failure_.type.decode('utf-8'), + message=str(failure_.value) + ) + + def call(self, name, *args, **kwargs): + d = self.root.callRemote(name, *args, *kwargs) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + async def bridge_connect(self): + d = super().bridge_connect(callback=None, errback=None) + return await d.asFuture(asyncio.get_event_loop()) + + def action_launch(self, callback_id, data, profile_key="@DEFAULT@"): + d = self.root.callRemote("action_launch", callback_id, data, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def actions_get(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("actions_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def config_get(self, section, name): + d = self.root.callRemote("config_get", section, name) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def connect(self, profile_key="@DEFAULT@", password='', options={}): + d = self.root.callRemote("connect", profile_key, password, options) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contact_add(self, entity_jid, profile_key="@DEFAULT@"): + d = self.root.callRemote("contact_add", entity_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contact_del(self, entity_jid, profile_key="@DEFAULT@"): + d = self.root.callRemote("contact_del", entity_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contact_get(self, arg_0, profile_key="@DEFAULT@"): + d = self.root.callRemote("contact_get", arg_0, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"): + d = self.root.callRemote("contact_update", entity_jid, name, groups, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contacts_get(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("contacts_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def contacts_get_from_group(self, group, profile_key="@DEFAULT@"): + d = self.root.callRemote("contacts_get_from_group", group, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def devices_infos_get(self, bare_jid, profile_key): + d = self.root.callRemote("devices_infos_get", bare_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@"): + d = self.root.callRemote("disco_find_by_features", namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + d = self.root.callRemote("disco_infos", entity_jid, node, use_cache, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + d = self.root.callRemote("disco_items", entity_jid, node, use_cache, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def disconnect(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("disconnect", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def encryption_namespace_get(self, arg_0): + d = self.root.callRemote("encryption_namespace_get", arg_0) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def encryption_plugins_get(self): + d = self.root.callRemote("encryption_plugins_get") + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def encryption_trust_ui_get(self, to_jid, namespace, profile_key): + d = self.root.callRemote("encryption_trust_ui_get", to_jid, namespace, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def entities_data_get(self, jids, keys, profile): + d = self.root.callRemote("entities_data_get", jids, keys, profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def entity_data_get(self, jid, keys, profile): + d = self.root.callRemote("entity_data_get", jid, keys, profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def features_get(self, profile_key): + d = self.root.callRemote("features_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@"): + d = self.root.callRemote("history_get", from_jid, to_jid, limit, between, filters, profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def image_check(self, arg_0): + d = self.root.callRemote("image_check", arg_0) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def image_convert(self, source, dest, arg_2, extra): + d = self.root.callRemote("image_convert", source, dest, arg_2, extra) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def image_generate_preview(self, image_path, profile_key): + d = self.root.callRemote("image_generate_preview", image_path, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def image_resize(self, image_path, width, height): + d = self.root.callRemote("image_resize", image_path, width, height) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def is_connected(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("is_connected", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"): + d = self.root.callRemote("main_resource_get", contact_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def menu_help_get(self, menu_id, language): + d = self.root.callRemote("menu_help_get", menu_id, language) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def menu_launch(self, menu_type, path, data, security_limit, profile_key): + d = self.root.callRemote("menu_launch", menu_type, path, data, security_limit, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def menus_get(self, language, security_limit): + d = self.root.callRemote("menus_get", language, security_limit) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def message_encryption_get(self, to_jid, profile_key): + d = self.root.callRemote("message_encryption_get", to_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): + d = self.root.callRemote("message_encryption_start", to_jid, namespace, replace, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def message_encryption_stop(self, to_jid, profile_key): + d = self.root.callRemote("message_encryption_stop", to_jid, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@"): + d = self.root.callRemote("message_send", to_jid, message, subject, mess_type, extra, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def namespaces_get(self): + d = self.root.callRemote("namespaces_get") + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"): + d = self.root.callRemote("param_get_a", name, category, attribute, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@"): + d = self.root.callRemote("param_get_a_async", name, category, attribute, security_limit, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): + d = self.root.callRemote("param_set", name, value, category, security_limit, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"): + d = self.root.callRemote("param_ui_get", security_limit, app, extra, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def params_categories_get(self): + d = self.root.callRemote("params_categories_get") + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def params_register_app(self, xml, security_limit=-1, app=''): + d = self.root.callRemote("params_register_app", xml, security_limit, app) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def params_template_load(self, filename): + d = self.root.callRemote("params_template_load", filename) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def params_template_save(self, filename): + d = self.root.callRemote("params_template_save", filename) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"): + d = self.root.callRemote("params_values_from_category_get_async", category, security_limit, app, extra, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): + d = self.root.callRemote("presence_set", to_jid, show, statuses, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def presence_statuses_get(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("presence_statuses_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def private_data_delete(self, namespace, key, arg_2): + d = self.root.callRemote("private_data_delete", namespace, key, arg_2) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def private_data_get(self, namespace, key, profile_key): + d = self.root.callRemote("private_data_get", namespace, key, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def private_data_set(self, namespace, key, data, profile_key): + d = self.root.callRemote("private_data_set", namespace, key, data, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_create(self, profile, password='', component=''): + d = self.root.callRemote("profile_create", profile, password, component) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_delete_async(self, profile): + d = self.root.callRemote("profile_delete_async", profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_is_session_started(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("profile_is_session_started", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_name_get(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("profile_name_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_set_default(self, profile): + d = self.root.callRemote("profile_set_default", profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profile_start_session(self, password='', profile_key="@DEFAULT@"): + d = self.root.callRemote("profile_start_session", password, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def profiles_list_get(self, clients=True, components=False): + d = self.root.callRemote("profiles_list_get", clients, components) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def progress_get(self, id, profile): + d = self.root.callRemote("progress_get", id, profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def progress_get_all(self, profile): + d = self.root.callRemote("progress_get_all", profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def progress_get_all_metadata(self, profile): + d = self.root.callRemote("progress_get_all_metadata", profile) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def ready_get(self): + d = self.root.callRemote("ready_get") + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def roster_resync(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("roster_resync", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def session_infos_get(self, profile_key): + d = self.root.callRemote("session_infos_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def sub_waiting_get(self, profile_key="@DEFAULT@"): + d = self.root.callRemote("sub_waiting_get", profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): + d = self.root.callRemote("subscription", sub_type, entity, profile_key) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + def version_get(self): + d = self.root.callRemote("version_get") + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/arg_tools.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/arg_tools.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +from libervia.backend.core import exceptions + + +def escape(arg, smart=True): + """format arg with quotes + + @param smart(bool): if True, only escape if needed + """ + if smart and not " " in arg and not '"' in arg: + return arg + return '"' + arg.replace('"', '\\"') + '"' + + +def get_cmd_choices(cmd=None, parser=None): + try: + choices = parser._subparsers._group_actions[0].choices + return choices[cmd] if cmd is not None else choices + except (KeyError, AttributeError): + raise exceptions.NotFound + + +def get_use_args(host, args, use, verbose=False, parser=None): + """format args for argparse parser with values prefilled + + @param host(JP): jp instance + @param args(list(str)): arguments to use + @param use(dict[str, str]): arguments to fill if found in parser + @param verbose(bool): if True a message will be displayed when argument is used or not + @param parser(argparse.ArgumentParser): parser to use + @return (tuple[list[str],list[str]]): 2 args lists: + - parser args, i.e. given args corresponding to parsers + - use args, i.e. generated args from use + """ + # FIXME: positional args are not handled correclty + # if there is more that one, the position is not corrected + if parser is None: + parser = host.parser + + # we check not optional args to see if there + # is a corresonding parser + # else USE args would not work correctly (only for current parser) + parser_args = [] + for arg in args: + if arg.startswith("-"): + break + try: + parser = get_cmd_choices(arg, parser) + except exceptions.NotFound: + break + parser_args.append(arg) + + # post_args are remaning given args, + # without the ones corresponding to parsers + post_args = args[len(parser_args) :] + + opt_args = [] + pos_args = [] + actions = {a.dest: a for a in parser._actions} + for arg, value in use.items(): + try: + if arg == "item" and not "item" in actions: + # small hack when --item is appended to a --items list + arg = "items" + action = actions[arg] + except KeyError: + if verbose: + host.disp( + _( + "ignoring {name}={value}, not corresponding to any argument (in USE)" + ).format(name=arg, value=escape(value)) + ) + else: + if verbose: + host.disp( + _("arg {name}={value} (in USE)").format( + name=arg, value=escape(value) + ) + ) + if not action.option_strings: + pos_args.append(value) + else: + opt_args.append(action.option_strings[0]) + opt_args.append(value) + return parser_args, opt_args + pos_args + post_args diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/base.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/base.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1435 @@ +#!/usr/bin/env python3 + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +from libervia.backend.core.i18n import _ + +### logging ### +import logging as log +log.basicConfig(level=log.WARNING, + format='[%(name)s] %(message)s') +### + +import sys +import os +import os.path +import argparse +import inspect +import tty +import termios +from pathlib import Path +from glob import iglob +from typing import Optional, Set, Union +from importlib import import_module +from libervia.frontends.tools.jid import JID +from libervia.backend.tools import config +from libervia.backend.tools.common import dynamic_import +from libervia.backend.tools.common import uri +from libervia.backend.tools.common import date_utils +from libervia.backend.tools.common import utils +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.core import exceptions +import libervia.frontends.jp +from libervia.frontends.jp.loops import QuitException, get_jp_loop +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.bridge.bridge_frontend import BridgeException +from libervia.frontends.tools import misc +import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI +from collections import OrderedDict + +## bridge handling +# we get bridge name from conf and initialise the right class accordingly +main_config = config.parse_main_conf() +bridge_name = config.config_get(main_config, '', 'bridge', 'dbus') +JPLoop = get_jp_loop(bridge_name) + + +try: + import progressbar +except ImportError: + msg = (_('ProgressBar not available, please download it at ' + 'http://pypi.python.org/pypi/progressbar\n' + 'Progress bar deactivated\n--\n')) + print(msg, file=sys.stderr) + progressbar=None + +#consts +DESCRIPTION = """This software is a command line tool for XMPP. +Get the latest version at """ + C.APP_URL + +COPYLEFT = """Copyright (C) 2009-2021 Jérôme Poisson, Adrien Cossa +This program comes with ABSOLUTELY NO WARRANTY; +This is free software, and you are welcome to redistribute it under certain conditions. +""" + +PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s + + +def date_decoder(arg): + return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) + + +class LiberviaCli: + """ + This class can be use to establish a connection with the + bridge. Moreover, it should manage a main loop. + + To use it, you mainly have to redefine the method run to perform + specify what kind of operation you want to perform. + + """ + def __init__(self): + """ + + @attribute quit_on_progress_end (bool): set to False if you manage yourself + exiting, or if you want the user to stop by himself + @attribute progress_success(callable): method to call when progress just started + by default display a message + @attribute progress_success(callable): method to call when progress is + successfully finished by default display a message + @attribute progress_failure(callable): method to call when progress failed + by default display a message + """ + self.sat_conf = main_config + self.set_color_theme() + bridge_module = dynamic_import.bridge(bridge_name, 'libervia.frontends.bridge') + if bridge_module is None: + log.error("Can't import {} bridge".format(bridge_name)) + sys.exit(1) + + self.bridge = bridge_module.AIOBridge() + self._onQuitCallbacks = [] + + def get_config(self, name, section=C.CONFIG_SECTION, default=None): + """Retrieve a setting value from sat.conf""" + return config.config_get(self.sat_conf, section, name, default=default) + + def guess_background(self): + # cf. https://unix.stackexchange.com/a/245568 (thanks!) + try: + # for VTE based terminals + vte_version = int(os.getenv("VTE_VERSION", 0)) + except ValueError: + vte_version = 0 + + color_fg_bg = os.getenv("COLORFGBG") + + if ((sys.stdin.isatty() and sys.stdout.isatty() + and ( + # XTerm + os.getenv("XTERM_VERSION") + # Konsole + or os.getenv("KONSOLE_VERSION") + # All VTE based terminals + or vte_version >= 3502 + ))): + # ANSI escape sequence + stdin_fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(stdin_fd) + try: + tty.setraw(sys.stdin.fileno()) + # we request background color + sys.stdout.write("\033]11;?\a") + sys.stdout.flush() + expected = "\033]11;rgb:" + for c in expected: + ch = sys.stdin.read(1) + if ch != c: + # background id is not supported, we default to "dark" + # TODO: log something? + return 'dark' + red, green, blue = [ + int(c, 16)/65535 for c in sys.stdin.read(14).split('/') + ] + # '\a' is the last character + sys.stdin.read(1) + finally: + termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) + + lum = utils.per_luminance(red, green, blue) + if lum <= 0.5: + return 'dark' + else: + return 'light' + elif color_fg_bg: + # no luck with ANSI escape sequence, we try COLORFGBG environment variable + try: + bg = int(color_fg_bg.split(";")[-1]) + except ValueError: + return "dark" + if bg in list(range(7)) + [8]: + return "dark" + else: + return "light" + else: + # no autodetection method found + return "dark" + + def set_color_theme(self): + background = self.get_config('background', default='auto') + if background == 'auto': + background = self.guess_background() + if background not in ('dark', 'light'): + raise exceptions.ConfigError(_( + 'Invalid value set for "background" ({background}), please check ' + 'your settings in libervia.conf').format( + background=repr(background) + )) + self.background = background + if background == 'light': + C.A_HEADER = A.FG_MAGENTA + C.A_SUBHEADER = A.BOLD + A.FG_RED + C.A_LEVEL_COLORS = (C.A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN) + C.A_SUCCESS = A.FG_GREEN + C.A_FAILURE = A.BOLD + A.FG_RED + C.A_WARNING = A.FG_RED + C.A_PROMPT_PATH = A.FG_BLUE + C.A_PROMPT_SUF = A.BOLD + C.A_DIRECTORY = A.BOLD + A.FG_MAGENTA + C.A_FILE = A.FG_BLACK + + def _bridge_connected(self): + self.parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) + self._make_parents() + self.add_parser_options() + self.subparsers = self.parser.add_subparsers( + title=_('Available commands'), dest='command', required=True) + + # progress attributes + self._progress_id = None # TODO: manage several progress ids + self.quit_on_progress_end = True + + # outputs + self._outputs = {} + for type_ in C.OUTPUT_TYPES: + self._outputs[type_] = OrderedDict() + self.default_output = {} + + self.own_jid = None # must be filled at runtime if needed + + @property + def progress_id(self): + return self._progress_id + + async def set_progress_id(self, progress_id): + # because we use async, we need an explicit setter + self._progress_id = progress_id + await self.replay_cache('progress_ids_cache') + + @property + def watch_progress(self): + try: + self.pbar + except AttributeError: + return False + else: + return True + + @watch_progress.setter + def watch_progress(self, watch_progress): + if watch_progress: + self.pbar = None + + @property + def verbosity(self): + try: + return self.args.verbose + except AttributeError: + return 0 + + async def replay_cache(self, cache_attribute): + """Replay cached signals + + @param cache_attribute(str): name of the attribute containing the cache + if the attribute doesn't exist, there is no cache and the call is ignored + else the cache must be a list of tuples containing the replay callback as + first item, then the arguments to use + """ + try: + cache = getattr(self, cache_attribute) + except AttributeError: + pass + else: + for cache_data in cache: + await cache_data[0](*cache_data[1:]) + + def disp(self, msg, verbosity=0, error=False, end='\n'): + """Print a message to user + + @param msg(unicode): message to print + @param verbosity(int): minimal verbosity to display the message + @param error(bool): if True, print to stderr instead of stdout + @param end(str): string appended after the last value, default a newline + """ + if self.verbosity >= verbosity: + if error: + print(msg, end=end, file=sys.stderr) + else: + print(msg, end=end) + + async def output(self, type_, name, extra_outputs, data): + if name in extra_outputs: + method = extra_outputs[name] + else: + method = self._outputs[type_][name]['callback'] + + ret = method(data) + if inspect.isawaitable(ret): + await ret + + def add_on_quit_callback(self, callback, *args, **kwargs): + """Add a callback which will be called on quit command + + @param callback(callback): method to call + """ + self._onQuitCallbacks.append((callback, args, kwargs)) + + def get_output_choices(self, output_type): + """Return valid output filters for output_type + + @param output_type: True for default, + else can be any registered type + """ + return list(self._outputs[output_type].keys()) + + def _make_parents(self): + self.parents = {} + + # we have a special case here as the start-session option is present only if + # connection is not needed, so we create two similar parents, one with the + # option, the other one without it + for parent_name in ('profile', 'profile_session'): + parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) + parent.add_argument( + "-p", "--profile", action="store", type=str, default='@DEFAULT@', + help=_("Use PROFILE profile key (default: %(default)s)")) + parent.add_argument( + "--pwd", action="store", metavar='PASSWORD', + help=_("Password used to connect profile, if necessary")) + + profile_parent, profile_session_parent = (self.parents['profile'], + self.parents['profile_session']) + + connect_short, connect_long, connect_action, connect_help = ( + "-c", "--connect", "store_true", + _("Connect the profile before doing anything else") + ) + profile_parent.add_argument( + connect_short, connect_long, action=connect_action, help=connect_help) + + profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() + profile_session_connect_group.add_argument( + connect_short, connect_long, action=connect_action, help=connect_help) + profile_session_connect_group.add_argument( + "--start-session", action="store_true", + help=_("Start a profile session without connecting")) + + progress_parent = self.parents['progress'] = argparse.ArgumentParser( + add_help=False) + if progressbar: + progress_parent.add_argument( + "-P", "--progress", action="store_true", help=_("Show progress bar")) + + verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) + verbose_parent.add_argument( + '--verbose', '-v', action='count', default=0, + help=_("Add a verbosity level (can be used multiple times)")) + + quiet_parent = self.parents['quiet'] = argparse.ArgumentParser(add_help=False) + quiet_parent.add_argument( + '--quiet', '-q', action='store_true', + help=_("be quiet (only output machine readable data)")) + + draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) + draft_group = draft_parent.add_argument_group(_('draft handling')) + draft_group.add_argument( + "-D", "--current", action="store_true", help=_("load current draft")) + draft_group.add_argument( + "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")) + + + def make_pubsub_group(self, flags, defaults): + """Generate pubsub options according to flags + + @param flags(iterable[unicode]): see [CommandBase.__init__] + @param defaults(dict[unicode, unicode]): help text for default value + key can be "service" or "node" + value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT + @return (ArgumentParser): parser to add + """ + flags = misc.FlagsHandler(flags) + parent = argparse.ArgumentParser(add_help=False) + pubsub_group = parent.add_argument_group('pubsub') + pubsub_group.add_argument("-u", "--pubsub-url", + help=_("Pubsub URL (xmpp or http)")) + + service_help = _("JID of the PubSub service") + if not flags.service: + default = defaults.pop('service', _('PEP service')) + if default is not None: + service_help += _(" (DEFAULT: {default})".format(default=default)) + pubsub_group.add_argument("-s", "--service", default='', + help=service_help) + + node_help = _("node to request") + if not flags.node: + default = defaults.pop('node', _('standard node')) + if default is not None: + node_help += _(" (DEFAULT: {default})".format(default=default)) + pubsub_group.add_argument("-n", "--node", default='', help=node_help) + + if flags.single_item: + item_help = ("item to retrieve") + if not flags.item: + default = defaults.pop('item', _('last item')) + if default is not None: + item_help += _(" (DEFAULT: {default})".format(default=default)) + pubsub_group.add_argument("-i", "--item", default='', + help=item_help) + pubsub_group.add_argument( + "-L", "--last-item", action='store_true', help=_('retrieve last item')) + elif flags.multi_items: + # mutiple items, this activate several features: max-items, RSM, MAM + # and Orbder-by + pubsub_group.add_argument( + "-i", "--item", action='append', dest='items', default=[], + help=_("items to retrieve (DEFAULT: all)")) + if not flags.no_max: + max_group = pubsub_group.add_mutually_exclusive_group() + # XXX: defaut value for --max-items or --max is set in parse_pubsub_args + max_group.add_argument( + "-M", "--max-items", dest="max", type=int, + help=_("maximum number of items to get ({no_limit} to get all items)" + .format(no_limit=C.NO_LIMIT))) + # FIXME: it could be possible to no duplicate max (between pubsub + # max-items and RSM max)should not be duplicated, RSM could be + # used when available and pubsub max otherwise + max_group.add_argument( + "-m", "--max", dest="rsm_max", type=int, + help=_("maximum number of items to get per page (DEFAULT: 10)")) + + # RSM + + rsm_page_group = pubsub_group.add_mutually_exclusive_group() + rsm_page_group.add_argument( + "-a", "--after", dest="rsm_after", + help=_("find page after this item"), metavar='ITEM_ID') + rsm_page_group.add_argument( + "-b", "--before", dest="rsm_before", + help=_("find page before this item"), metavar='ITEM_ID') + rsm_page_group.add_argument( + "--index", dest="rsm_index", type=int, + help=_("index of the first item to retrieve")) + + + # MAM + + pubsub_group.add_argument( + "-f", "--filter", dest='mam_filters', nargs=2, + action='append', default=[], help=_("MAM filters to use"), + metavar=("FILTER_NAME", "VALUE") + ) + + # Order-By + + # TODO: order-by should be a list to handle several levels of ordering + # but this is not yet done in SàT (and not really useful with + # current specifications, as only "creation" and "modification" are + # available) + pubsub_group.add_argument( + "-o", "--order-by", choices=[C.ORDER_BY_CREATION, + C.ORDER_BY_MODIFICATION], + help=_("how items should be ordered")) + + if flags[C.CACHE]: + pubsub_group.add_argument( + "-C", "--no-cache", dest="use_cache", action='store_false', + help=_("don't use Pubsub cache") + ) + + if not flags.all_used: + raise exceptions.InternalError('unknown flags: {flags}'.format( + flags=', '.join(flags.unused))) + if defaults: + raise exceptions.InternalError(f'unused defaults: {defaults}') + + return parent + + def add_parser_options(self): + self.parser.add_argument( + '--version', + action='version', + version=("{name} {version} {copyleft}".format( + name = C.APP_NAME, + version = self.version, + copyleft = COPYLEFT)) + ) + + def register_output(self, type_, name, callback, description="", default=False): + if type_ not in C.OUTPUT_TYPES: + log.error("Invalid output type {}".format(type_)) + return + self._outputs[type_][name] = {'callback': callback, + 'description': description + } + if default: + if type_ in self.default_output: + self.disp( + _('there is already a default output for {type}, ignoring new one') + .format(type=type_) + ) + else: + self.default_output[type_] = name + + + def parse_output_options(self): + options = self.command.args.output_opts + options_dict = {} + for option in options: + try: + key, value = option.split('=', 1) + except ValueError: + key, value = option, None + options_dict[key.strip()] = value.strip() if value is not None else None + return options_dict + + def check_output_options(self, accepted_set, options): + if not accepted_set.issuperset(options): + self.disp( + _("The following output options are invalid: {invalid_options}").format( + invalid_options = ', '.join(set(options).difference(accepted_set))), + error=True) + self.quit(C.EXIT_BAD_ARG) + + def import_plugins(self): + """Automaticaly import commands and outputs in jp + + looks from modules names cmd_*.py in jp path and import them + """ + path = os.path.dirname(libervia.frontends.jp.__file__) + # XXX: outputs must be imported before commands as they are used for arguments + for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), + (C.PLUGIN_CMD, 'cmd_*.py')): + modules = ( + os.path.splitext(module)[0] + for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) + for module_name in modules: + module_path = "libervia.frontends.jp." + module_name + try: + module = import_module(module_path) + self.import_plugin_module(module, type_) + except ImportError as e: + self.disp( + _("Can't import {module_path} plugin, ignoring it: {e}") + .format(module_path=module_path, e=e), + error=True) + except exceptions.CancelError: + continue + except exceptions.MissingModule as e: + self.disp(_("Missing module for plugin {name}: {missing}".format( + name = module_path, + missing = e)), error=True) + + + def import_plugin_module(self, module, type_): + """add commands or outpus from a module to jp + + @param module: module containing commands or outputs + @param type_(str): one of C_PLUGIN_* + """ + try: + class_names = getattr(module, '__{}__'.format(type_)) + except AttributeError: + log.disp( + _("Invalid plugin module [{type}] {module}") + .format(type=type_, module=module), + error=True) + raise ImportError + else: + for class_name in class_names: + cls = getattr(module, class_name) + cls(self) + + def get_xmpp_uri_from_http(self, http_url): + """parse HTML page at http(s) URL, and looks for xmpp: uri""" + if http_url.startswith('https'): + scheme = 'https' + elif http_url.startswith('http'): + scheme = 'http' + else: + raise exceptions.InternalError('An HTTP scheme is expected in this method') + self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1) + # HTTP URL, we try to find xmpp: links + try: + from lxml import etree + except ImportError: + self.disp( + "lxml module must be installed to use http(s) scheme, please install it " + "with \"pip install lxml\"", + error=True) + self.quit(1) + import urllib.request, urllib.error, urllib.parse + parser = etree.HTMLParser() + try: + root = etree.parse(urllib.request.urlopen(http_url), parser) + except etree.XMLSyntaxError as e: + self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) + links = [] + else: + links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") + if not links: + self.disp( + _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP ' + 'PubSub node/item'), + error=True) + self.quit(1) + xmpp_uri = links[0].get('href') + return xmpp_uri + + def parse_pubsub_args(self): + if self.args.pubsub_url is not None: + url = self.args.pubsub_url + + if url.startswith('http'): + # http(s) URL, we try to retrieve xmpp one from there + url = self.get_xmpp_uri_from_http(url) + + try: + uri_data = uri.parse_xmpp_uri(url) + except ValueError: + self.parser.error(_('invalid XMPP URL: {url}').format(url=url)) + else: + if uri_data['type'] == 'pubsub': + # URL is alright, we only set data not already set by other options + if not self.args.service: + self.args.service = uri_data['path'] + if not self.args.node: + self.args.node = uri_data['node'] + uri_item = uri_data.get('item') + if uri_item: + # there is an item in URI + # we use it only if item is not already set + # and item_last is not used either + try: + item = self.args.item + except AttributeError: + try: + items = self.args.items + except AttributeError: + self.disp( + _("item specified in URL but not needed in command, " + "ignoring it"), + error=True) + else: + if not items: + self.args.items = [uri_item] + else: + if not item: + try: + item_last = self.args.item_last + except AttributeError: + item_last = False + if not item_last: + self.args.item = uri_item + else: + self.parser.error( + _('XMPP URL is not a pubsub one: {url}').format(url=url) + ) + flags = self.args._cmd._pubsub_flags + # we check required arguments here instead of using add_arguments' required option + # because the required argument can be set in URL + if C.SERVICE in flags and not self.args.service: + self.parser.error(_("argument -s/--service is required")) + if C.NODE in flags and not self.args.node: + self.parser.error(_("argument -n/--node is required")) + if C.ITEM in flags and not self.args.item: + self.parser.error(_("argument -i/--item is required")) + + # FIXME: mutually groups can't be nested in a group and don't support title + # so we check conflict here. This may be fixed in Python 3, to be checked + try: + if self.args.item and self.args.item_last: + self.parser.error( + _("--item and --item-last can't be used at the same time")) + except AttributeError: + pass + + try: + max_items = self.args.max + rsm_max = self.args.rsm_max + except AttributeError: + pass + else: + # we need to set a default value for max, but we need to know if we want + # to use pubsub's max or RSM's max. The later is used if any RSM or MAM + # argument is set + if max_items is None and rsm_max is None: + to_check = ('mam_filters', 'rsm_max', 'rsm_after', 'rsm_before', + 'rsm_index') + if any((getattr(self.args, name) for name in to_check)): + # we use RSM + self.args.rsm_max = 10 + else: + # we use pubsub without RSM + self.args.max = 10 + if self.args.max is None: + self.args.max = C.NO_LIMIT + + async def main(self, args, namespace): + try: + await self.bridge.bridge_connect() + except Exception as e: + if isinstance(e, exceptions.BridgeExceptionNoService): + print( + _("Can't connect to Libervia backend, are you sure that it's " + "launched ?") + ) + self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False) + elif isinstance(e, exceptions.BridgeInitError): + print(_("Can't init bridge")) + self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) + else: + print( + _("Error while initialising bridge: {e}").format(e=e) + ) + self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) + return + await self.bridge.ready_get() + self.version = await self.bridge.version_get() + self._bridge_connected() + self.import_plugins() + try: + self.args = self.parser.parse_args(args, namespace=None) + if self.args._cmd._use_pubsub: + self.parse_pubsub_args() + await self.args._cmd.run() + except SystemExit as e: + self.quit(e.code, raise_exc=False) + return + except QuitException: + return + + def _run(self, args=None, namespace=None): + self.loop = JPLoop() + self.loop.run(self, args, namespace) + + @classmethod + def run(cls): + cls()._run() + + def _read_stdin(self, stdin_fut): + """Callback called by ainput to read stdin""" + line = sys.stdin.readline() + if line: + stdin_fut.set_result(line.rstrip(os.linesep)) + else: + stdin_fut.set_exception(EOFError()) + + async def ainput(self, msg=''): + """Asynchronous version of buildin "input" function""" + self.disp(msg, end=' ') + sys.stdout.flush() + loop = asyncio.get_running_loop() + stdin_fut = loop.create_future() + loop.add_reader(sys.stdin, self._read_stdin, stdin_fut) + return await stdin_fut + + async def confirm(self, message): + """Request user to confirm action, return answer as boolean""" + res = await self.ainput(f"{message} (y/N)? ") + return res in ("y", "Y") + + async def confirm_or_quit(self, message, cancel_message=_("action cancelled by user")): + """Request user to confirm action, and quit if he doesn't""" + confirmed = await self.confirm(message) + if not confirmed: + self.disp(cancel_message) + self.quit(C.EXIT_USER_CANCELLED) + + def quit_from_signal(self, exit_code=0): + r"""Same as self.quit, but from a signal handler + + /!\: return must be used after calling this method ! + """ + # XXX: python-dbus will show a traceback if we exit in a signal handler + # so we use this little timeout trick to avoid it + self.loop.call_later(0, self.quit, exit_code) + + def quit(self, exit_code=0, raise_exc=True): + """Terminate the execution with specified exit_code + + This will stop the loop. + @param exit_code(int): code to return when quitting the program + @param raise_exp(boolean): if True raise a QuitException to stop code execution + The default value should be used most of time. + """ + # first the onQuitCallbacks + try: + callbacks_list = self._onQuitCallbacks + except AttributeError: + pass + else: + for callback, args, kwargs in callbacks_list: + callback(*args, **kwargs) + + self.loop.quit(exit_code) + if raise_exc: + raise QuitException + + async def check_jids(self, jids): + """Check jids validity, transform roster name to corresponding jids + + @param profile: profile name + @param jids: list of jids + @return: List of jids + + """ + names2jid = {} + nodes2jid = {} + + try: + contacts = await self.bridge.contacts_get(self.profile) + except BridgeException as e: + if e.classname == "AttributeError": + # we may get an AttributeError if we use a component profile + # as components don't have roster + contacts = [] + else: + raise e + + for contact in contacts: + jid_s, attr, groups = contact + _jid = JID(jid_s) + try: + names2jid[attr["name"].lower()] = jid_s + except KeyError: + pass + + if _jid.node: + nodes2jid[_jid.node.lower()] = jid_s + + def expand_jid(jid): + _jid = jid.lower() + if _jid in names2jid: + expanded = names2jid[_jid] + elif _jid in nodes2jid: + expanded = nodes2jid[_jid] + else: + expanded = jid + return expanded + + def check(jid): + if not jid.is_valid: + log.error (_("%s is not a valid JID !"), jid) + self.quit(1) + + dest_jids=[] + try: + for i in range(len(jids)): + dest_jids.append(expand_jid(jids[i])) + check(dest_jids[i]) + except AttributeError: + pass + + return dest_jids + + async def a_pwd_input(self, msg=''): + """Like ainput but with echo disabled (useful for passwords)""" + # we disable echo, code adapted from getpass standard module which has been + # written by Piers Lauder (original), Guido van Rossum (Windows support and + # cleanup) and Gregory P. Smith (tty support & GetPassWarning), a big thanks + # to them (and for all the amazing work on Python). + stdin_fd = sys.stdin.fileno() + old = termios.tcgetattr(sys.stdin) + new = old[:] + new[3] &= ~termios.ECHO + tcsetattr_flags = termios.TCSAFLUSH + if hasattr(termios, 'TCSASOFT'): + tcsetattr_flags |= termios.TCSASOFT + try: + termios.tcsetattr(stdin_fd, tcsetattr_flags, new) + pwd = await self.ainput(msg=msg) + finally: + termios.tcsetattr(stdin_fd, tcsetattr_flags, old) + sys.stderr.flush() + self.disp('') + return pwd + + async def connect_or_prompt(self, method, err_msg=None): + """Try to connect/start profile session and prompt for password if needed + + @param method(callable): bridge method to either connect or start profile session + It will be called with password as sole argument, use lambda to do the call + properly + @param err_msg(str): message to show if connection fail + """ + password = self.args.pwd + while True: + try: + await method(password or '') + except Exception as e: + if ((isinstance(e, BridgeException) + and e.classname == 'PasswordError' + and self.args.pwd is None)): + if password is not None: + self.disp(A.color(C.A_WARNING, _("invalid password"))) + password = await self.a_pwd_input( + _("please enter profile password:")) + else: + self.disp(err_msg.format(profile=self.profile, e=e), error=True) + self.quit(C.EXIT_ERROR) + else: + break + + async def connect_profile(self): + """Check if the profile is connected and do it if requested + + @exit: - 1 when profile is not connected and --connect is not set + - 1 when the profile doesn't exists + - 1 when there is a connection error + """ + # FIXME: need better exit codes + + self.profile = await self.bridge.profile_name_get(self.args.profile) + + if not self.profile: + log.error( + _("The profile [{profile}] doesn't exist") + .format(profile=self.args.profile) + ) + self.quit(C.EXIT_ERROR) + + try: + start_session = self.args.start_session + except AttributeError: + pass + else: + if start_session: + await self.connect_or_prompt( + lambda pwd: self.bridge.profile_start_session(pwd, self.profile), + err_msg="Can't start {profile}'s session: {e}" + ) + return + elif not await self.bridge.profile_is_session_started(self.profile): + if not self.args.connect: + self.disp(_( + "Session for [{profile}] is not started, please start it " + "before using jp, or use either --start-session or --connect " + "option" + .format(profile=self.profile) + ), error=True) + self.quit(1) + elif not getattr(self.args, "connect", False): + return + + + if not hasattr(self.args, 'connect'): + # a profile can be present without connect option (e.g. on profile + # creation/deletion) + return + elif self.args.connect is True: # if connection is asked, we connect the profile + await self.connect_or_prompt( + lambda pwd: self.bridge.connect(self.profile, pwd, {}), + err_msg = 'Can\'t connect profile "{profile!s}": {e}' + ) + return + else: + if not await self.bridge.is_connected(self.profile): + log.error( + _("Profile [{profile}] is not connected, please connect it " + "before using jp, or use --connect option") + .format(profile=self.profile) + ) + self.quit(1) + + async def get_full_jid(self, param_jid): + """Return the full jid if possible (add main resource when find a bare jid)""" + # TODO: to be removed, bare jid should work with all commands, notably for file + # as backend now handle jingles message initiation + _jid = JID(param_jid) + if not _jid.resource: + #if the resource is not given, we try to add the main resource + main_resource = await self.bridge.main_resource_get(param_jid, self.profile) + if main_resource: + return f"{_jid.bare}/{main_resource}" + return param_jid + + async def get_profile_jid(self): + """Retrieve current profile bare JID if possible""" + full_jid = await self.bridge.param_get_a_async( + "JabberID", "Connection", profile_key=self.profile + ) + return full_jid.rsplit("/", 1)[0] + + +class CommandBase: + + def __init__( + self, + host: LiberviaCli, + name: str, + use_profile: bool = True, + use_output: Union[bool, str] = False, + extra_outputs: Optional[dict] = None, + need_connect: Optional[bool] = None, + help: Optional[str] = None, + **kwargs + ): + """Initialise CommandBase + + @param host: Jp instance + @param name: name of the new command + @param use_profile: if True, add profile selection/connection commands + @param use_output: if not False, add --output option + @param extra_outputs: list of command specific outputs: + key is output name ("default" to use as main output) + value is a callable which will format the output (data will be used as only + argument) + if a key already exists with normal outputs, the extra one will be used + @param need_connect: True if profile connection is needed + False else (profile session must still be started) + None to set auto value (i.e. True if use_profile is set) + Can't be set if use_profile is False + @param help: help message to display + @param **kwargs: args passed to ArgumentParser + use_* are handled directly, they can be: + - use_progress(bool): if True, add progress bar activation option + progress* signals will be handled + - use_verbose(bool): if True, add verbosity option + - use_pubsub(bool): if True, add pubsub options + mandatory arguments are controlled by pubsub_req + - use_draft(bool): if True, add draft handling options + ** other arguments ** + - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, + can be: + C.SERVICE: service is required + C.NODE: node is required + C.ITEM: item is required + C.SINGLE_ITEM: only one item is allowed + """ + try: # If we have subcommands, host is a CommandBase and we need to use host.host + self.host = host.host + except AttributeError: + self.host = host + + # --profile option + parents = kwargs.setdefault('parents', set()) + if use_profile: + # self.host.parents['profile'] is an ArgumentParser with profile connection + # arguments + if need_connect is None: + need_connect = True + parents.add( + self.host.parents['profile' if need_connect else 'profile_session']) + else: + assert need_connect is None + self.need_connect = need_connect + # from this point, self.need_connect is None if connection is not needed at all + # False if session starting is needed, and True if full connection is needed + + # --output option + if use_output: + if extra_outputs is None: + extra_outputs = {} + self.extra_outputs = extra_outputs + if use_output == True: + use_output = C.OUTPUT_TEXT + assert use_output in C.OUTPUT_TYPES + self._output_type = use_output + output_parent = argparse.ArgumentParser(add_help=False) + choices = set(self.host.get_output_choices(use_output)) + choices.update(extra_outputs) + if not choices: + raise exceptions.InternalError( + "No choice found for {} output type".format(use_output)) + try: + default = self.host.default_output[use_output] + except KeyError: + if 'default' in choices: + default = 'default' + elif 'simple' in choices: + default = 'simple' + else: + default = list(choices)[0] + output_parent.add_argument( + '--output', '-O', choices=sorted(choices), default=default, + help=_("select output format (default: {})".format(default))) + output_parent.add_argument( + '--output-option', '--oo', action="append", dest='output_opts', + default=[], help=_("output specific option")) + parents.add(output_parent) + else: + assert extra_outputs is None + + self._use_pubsub = kwargs.pop('use_pubsub', False) + if self._use_pubsub: + flags = kwargs.pop('pubsub_flags', []) + defaults = kwargs.pop('pubsub_defaults', {}) + parents.add(self.host.make_pubsub_group(flags, defaults)) + self._pubsub_flags = flags + + # other common options + use_opts = {k:v for k,v in kwargs.items() if k.startswith('use_')} + for param, do_use in use_opts.items(): + opt=param[4:] # if param is use_verbose, opt is verbose + if opt not in self.host.parents: + raise exceptions.InternalError("Unknown parent option {}".format(opt)) + del kwargs[param] + if do_use: + parents.add(self.host.parents[opt]) + + self.parser = host.subparsers.add_parser(name, help=help, **kwargs) + if hasattr(self, "subcommands"): + self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True) + else: + self.parser.set_defaults(_cmd=self) + self.add_parser_options() + + @property + def sat_conf(self): + return self.host.sat_conf + + @property + def args(self): + return self.host.args + + @property + def profile(self): + return self.host.profile + + @property + def verbosity(self): + return self.host.verbosity + + @property + def progress_id(self): + return self.host.progress_id + + async def set_progress_id(self, progress_id): + return await self.host.set_progress_id(progress_id) + + async def progress_started_handler(self, uid, metadata, profile): + if profile != self.profile: + return + if self.progress_id is None: + # the progress started message can be received before the id + # so we keep progress_started signals in cache to replay they + # when the progress_id is received + cache_data = (self.progress_started_handler, uid, metadata, profile) + try: + cache = self.host.progress_ids_cache + except AttributeError: + cache = self.host.progress_ids_cache = [] + cache.append(cache_data) + else: + if self.host.watch_progress and uid == self.progress_id: + await self.on_progress_started(metadata) + while True: + await asyncio.sleep(PROGRESS_DELAY) + cont = await self.progress_update() + if not cont: + break + + async def progress_finished_handler(self, uid, metadata, profile): + if profile != self.profile: + return + if uid == self.progress_id: + try: + self.host.pbar.finish() + except AttributeError: + pass + await self.on_progress_finished(metadata) + if self.host.quit_on_progress_end: + self.host.quit_from_signal() + + async def progress_error_handler(self, uid, message, profile): + if profile != self.profile: + return + if uid == self.progress_id: + if self.args.progress: + self.disp('') # progress is not finished, so we skip a line + if self.host.quit_on_progress_end: + await self.on_progress_error(message) + self.host.quit_from_signal(C.EXIT_ERROR) + + async def progress_update(self): + """This method is continualy called to update the progress bar + + @return (bool): False to stop being called + """ + data = await self.host.bridge.progress_get(self.progress_id, self.profile) + if data: + try: + size = data['size'] + except KeyError: + self.disp(_("file size is not known, we can't show a progress bar"), 1, + error=True) + return False + if self.host.pbar is None: + #first answer, we must construct the bar + + # if the instance has a pbar_template attribute, it is used has model, + # else default one is used + # template is a list of part, where part can be either a str to show directly + # or a list where first argument is a name of a progressbar widget, and others + # are used as widget arguments + try: + template = self.pbar_template + except AttributeError: + template = [ + _("Progress: "), ["Percentage"], " ", ["Bar"], " ", + ["FileTransferSpeed"], " ", ["ETA"] + ] + + widgets = [] + for part in template: + if isinstance(part, str): + widgets.append(part) + else: + widget = getattr(progressbar, part.pop(0)) + widgets.append(widget(*part)) + + self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets) + self.host.pbar.start() + + self.host.pbar.update(int(data['position'])) + + elif self.host.pbar is not None: + return False + + await self.on_progress_update(data) + + return True + + async def on_progress_started(self, metadata): + """Called when progress has just started + + can be overidden by a command + @param metadata(dict): metadata as sent by bridge.progress_started + """ + self.disp(_("Operation started"), 2) + + async def on_progress_update(self, metadata): + """Method called on each progress updata + + can be overidden by a command to handle progress metadata + @para metadata(dict): metadata as returned by bridge.progress_get + """ + pass + + async def on_progress_finished(self, metadata): + """Called when progress has just finished + + can be overidden by a command + @param metadata(dict): metadata as sent by bridge.progress_finished + """ + self.disp(_("Operation successfully finished"), 2) + + async def on_progress_error(self, e): + """Called when a progress failed + + @param error_msg(unicode): error message as sent by bridge.progress_error + """ + self.disp(_("Error while doing operation: {e}").format(e=e), error=True) + + def disp(self, msg, verbosity=0, error=False, end='\n'): + return self.host.disp(msg, verbosity, error, end) + + def output(self, data): + try: + output_type = self._output_type + except AttributeError: + raise exceptions.InternalError( + _('trying to use output when use_output has not been set')) + return self.host.output(output_type, self.args.output, self.extra_outputs, data) + + def get_pubsub_extra(self, extra: Optional[dict] = None) -> str: + """Helper method to compute extra data from pubsub arguments + + @param extra: base extra dict, or None to generate a new one + @return: serialised dict which can be used directly in the bridge for pubsub + """ + if extra is None: + extra = {} + else: + intersection = {C.KEY_ORDER_BY}.intersection(list(extra.keys())) + if intersection: + raise exceptions.ConflictError( + "given extra dict has conflicting keys with pubsub keys " + "{intersection}".format(intersection=intersection)) + + # RSM + + for attribute in ('max', 'after', 'before', 'index'): + key = 'rsm_' + attribute + if key in extra: + raise exceptions.ConflictError( + "This key already exists in extra: u{key}".format(key=key)) + value = getattr(self.args, key, None) + if value is not None: + extra[key] = str(value) + + # MAM + + if hasattr(self.args, 'mam_filters'): + for key, value in self.args.mam_filters: + key = 'filter_' + key + if key in extra: + raise exceptions.ConflictError( + "This key already exists in extra: u{key}".format(key=key)) + extra[key] = value + + # Order-By + + try: + order_by = self.args.order_by + except AttributeError: + pass + else: + if order_by is not None: + extra[C.KEY_ORDER_BY] = self.args.order_by + + # Cache + try: + use_cache = self.args.use_cache + except AttributeError: + pass + else: + if not use_cache: + extra[C.KEY_USE_CACHE] = use_cache + + return data_format.serialise(extra) + + def add_parser_options(self): + try: + subcommands = self.subcommands + except AttributeError: + # We don't have subcommands, the class need to implements add_parser_options + raise NotImplementedError + + # now we add subcommands to ourself + for cls in subcommands: + cls(self) + + def override_pubsub_flags(self, new_flags: Set[str]) -> None: + """Replace pubsub_flags given in __init__ + + useful when a command is extending an other command (e.g. blog command which does + the same as pubsub command, but with a default node) + """ + self._pubsub_flags = new_flags + + async def run(self): + """this method is called when a command is actually run + + It set stuff like progression callbacks and profile connection + You should not overide this method: you should call self.start instead + """ + # we keep a reference to run command, it may be useful e.g. for outputs + self.host.command = self + + try: + show_progress = self.args.progress + except AttributeError: + # the command doesn't use progress bar + pass + else: + if show_progress: + self.host.watch_progress = True + # we need to register the following signal even if we don't display the + # progress bar + self.host.bridge.register_signal( + "progress_started", self.progress_started_handler) + self.host.bridge.register_signal( + "progress_finished", self.progress_finished_handler) + self.host.bridge.register_signal( + "progress_error", self.progress_error_handler) + + if self.need_connect is not None: + await self.host.connect_profile() + await self.start() + + async def start(self): + """This is the starting point of the command, this method must be overriden + + at this point, profile are connected if needed + """ + raise NotImplementedError + + +class CommandAnswering(CommandBase): + """Specialised commands which answer to specific actions + + to manage action_types answer, + """ + action_callbacks = {} # XXX: set managed action types in a dict here: + # key is the action_type, value is the callable + # which will manage the answer. profile filtering is + # already managed when callback is called + + def __init__(self, *args, **kwargs): + super(CommandAnswering, self).__init__(*args, **kwargs) + + async def on_action_new( + self, + action_data_s: str, + action_id: str, + security_limit: int, + profile: str + ) -> None: + if profile != self.profile: + return + action_data = data_format.deserialise(action_data_s) + try: + action_type = action_data['type'] + except KeyError: + try: + xml_ui = action_data["xmlui"] + except KeyError: + pass + else: + self.on_xmlui(xml_ui) + else: + try: + callback = self.action_callbacks[action_type] + except KeyError: + pass + else: + await callback(action_data, action_id, security_limit, profile) + + def on_xmlui(self, xml_ui): + """Display a dialog received from the backend. + + @param xml_ui (unicode): dialog XML representation + """ + # FIXME: we temporarily use ElementTree, but a real XMLUI managing module + # should be available in the future + # TODO: XMLUI module + ui = ET.fromstring(xml_ui.encode('utf-8')) + dialog = ui.find("dialog") + if dialog is not None: + self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") + + async def start_answering(self): + """Auto reply to confirmation requests""" + self.host.bridge.register_signal("action_new", self.on_action_new) + actions = await self.host.bridge.actions_get(self.profile) + for action_data_s, action_id, security_limit in actions: + await self.on_action_new(action_data_s, action_id, security_limit, self.profile) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_account.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_account.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This module permits to manage XMPP accounts using in-band registration (XEP-0077)""" + +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.bridge.bridge_frontend import BridgeException +from libervia.backend.core.log import getLogger +from libervia.backend.core.i18n import _ +from libervia.frontends.jp import base +from libervia.frontends.tools import jid + + +log = getLogger(__name__) + +__commands__ = ["Account"] + + +class AccountCreate(base.CommandBase): + def __init__(self, host): + super(AccountCreate, self).__init__( + host, + "create", + use_profile=False, + use_verbose=True, + help=_("create a XMPP account"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "jid", help=_("jid to create") + ) + self.parser.add_argument( + "password", help=_("password of the account") + ) + self.parser.add_argument( + "-p", + "--profile", + help=_( + "create a profile to use this account (default: don't create profile)" + ), + ) + self.parser.add_argument( + "-e", + "--email", + default="", + help=_("email (usage depends of XMPP server)"), + ) + self.parser.add_argument( + "-H", + "--host", + default="", + help=_("server host (IP address or domain, default: use localhost)"), + ) + self.parser.add_argument( + "-P", + "--port", + type=int, + default=0, + help=_("server port (default: {port})").format( + port=C.XMPP_C2S_PORT + ), + ) + + async def start(self): + try: + await self.host.bridge.in_band_account_new( + self.args.jid, + self.args.password, + self.args.email, + self.args.host, + self.args.port, + ) + + except BridgeException as e: + if e.condition == 'conflict': + self.disp( + f"The account {self.args.jid} already exists", + error=True + ) + self.host.quit(C.EXIT_CONFLICT) + else: + self.disp( + f"can't create account on {self.args.host or 'localhost'!r} with jid " + f"{self.args.jid!r} using In-Band Registration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + + self.disp(_("XMPP account created"), 1) + + if self.args.profile is None: + self.host.quit() + + + self.disp(_("creating profile"), 2) + try: + await self.host.bridge.profile_create( + self.args.profile, + self.args.password, + "", + ) + except BridgeException as e: + if e.condition == 'conflict': + self.disp( + f"The profile {self.args.profile} already exists", + error=True + ) + self.host.quit(C.EXIT_CONFLICT) + else: + self.disp( + _("Can't create profile {profile} to associate with jid " + "{jid}: {e}").format( + profile=self.args.profile, + jid=self.args.jid, + e=e + ), + error=True, + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + + self.disp(_("profile created"), 1) + try: + await self.host.bridge.profile_start_session( + self.args.password, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't start profile session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.param_set( + "JabberID", + self.args.jid, + "Connection", + profile_key=self.args.profile, + ) + except Exception as e: + self.disp(f"can't set JabberID parameter: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.param_set( + "Password", + self.args.password, + "Connection", + profile_key=self.args.profile, + ) + except Exception as e: + self.disp(f"can't set Password parameter: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.disp( + f"profile {self.args.profile} successfully created and associated to the new " + f"account", 1) + self.host.quit() + + +class AccountModify(base.CommandBase): + def __init__(self, host): + super(AccountModify, self).__init__( + host, "modify", help=_("change password for XMPP account") + ) + + def add_parser_options(self): + self.parser.add_argument( + "password", help=_("new XMPP password") + ) + + async def start(self): + try: + await self.host.bridge.in_band_password_change( + self.args.password, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't change XMPP password: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class AccountDelete(base.CommandBase): + def __init__(self, host): + super(AccountDelete, self).__init__( + host, "delete", help=_("delete a XMPP account") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("delete account without confirmation"), + ) + + async def start(self): + try: + jid_str = await self.host.bridge.param_get_a_async( + "JabberID", + "Connection", + profile_key=self.profile, + ) + except Exception as e: + self.disp(f"can't get JID of the profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + jid_ = jid.JID(jid_str) + if not self.args.force: + message = ( + f"You are about to delete the XMPP account with jid {jid_!r}\n" + f"This is the XMPP account of profile {self.profile!r}\n" + f"Are you sure that you want to delete this account?" + ) + await self.host.confirm_or_quit(message, _("Account deletion cancelled")) + + try: + await self.host.bridge.in_band_unregister(jid_.domain, self.args.profile) + except Exception as e: + self.disp(f"can't delete XMPP account with jid {jid_!r}: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() + + +class Account(base.CommandBase): + subcommands = (AccountCreate, AccountModify, AccountDelete) + + def __init__(self, host): + super(Account, self).__init__( + host, "account", use_profile=False, help=("XMPP account management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_adhoc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_adhoc.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp import xmlui_manager + +__commands__ = ["AdHoc"] + +FLAG_LOOP = "LOOP" +MAGIC_BAREJID = "@PROFILE_BAREJID@" + + +class Remote(base.CommandBase): + def __init__(self, host): + super(Remote, self).__init__( + host, "remote", use_verbose=True, help=_("remote control a software") + ) + + def add_parser_options(self): + self.parser.add_argument("software", type=str, help=_("software name")) + self.parser.add_argument( + "-j", + "--jids", + nargs="*", + default=[], + help=_("jids allowed to use the command"), + ) + self.parser.add_argument( + "-g", + "--groups", + nargs="*", + default=[], + help=_("groups allowed to use the command"), + ) + self.parser.add_argument( + "--forbidden-groups", + nargs="*", + default=[], + help=_("groups that are *NOT* allowed to use the command"), + ) + self.parser.add_argument( + "--forbidden-jids", + nargs="*", + default=[], + help=_("jids that are *NOT* allowed to use the command"), + ) + self.parser.add_argument( + "-l", "--loop", action="store_true", help=_("loop on the commands") + ) + + async def start(self): + name = self.args.software.lower() + flags = [] + magics = {jid for jid in self.args.jids if jid.count("@") > 1} + magics.add(MAGIC_BAREJID) + jids = set(self.args.jids).difference(magics) + if self.args.loop: + flags.append(FLAG_LOOP) + try: + bus_name, methods = await self.host.bridge.ad_hoc_dbus_add_auto( + name, + list(jids), + self.args.groups, + magics, + self.args.forbidden_jids, + self.args.forbidden_groups, + flags, + self.profile, + ) + except Exception as e: + self.disp(f"can't create remote control: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if not bus_name: + self.disp(_("No bus name found"), 1) + self.host.quit(C.EXIT_NOT_FOUND) + else: + self.disp(_("Bus name found: [%s]" % bus_name), 1) + for method in methods: + path, iface, command = method + self.disp( + _("Command found: (path:{path}, iface: {iface}) [{command}]") + .format(path=path, iface=iface, command=command), + 1, + ) + self.host.quit() + + +class Run(base.CommandBase): + """Run an Ad-Hoc command""" + + def __init__(self, host): + super(Run, self).__init__( + host, "run", use_verbose=True, help=_("run an Ad-Hoc command") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-j", + "--jid", + default="", + help=_("jid of the service (default: profile's server"), + ) + self.parser.add_argument( + "-S", + "--submit", + action="append_const", + const=xmlui_manager.SUBMIT, + dest="workflow", + help=_("submit form/page"), + ) + self.parser.add_argument( + "-f", + "--field", + action="append", + nargs=2, + dest="workflow", + metavar=("KEY", "VALUE"), + help=_("field value"), + ) + self.parser.add_argument( + "node", + nargs="?", + default="", + help=_("node of the command (default: list commands)"), + ) + + async def start(self): + try: + xmlui_raw = await self.host.bridge.ad_hoc_run( + self.args.jid, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get ad-hoc commands list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + xmlui = xmlui_manager.create(self.host, xmlui_raw) + workflow = self.args.workflow + await xmlui.show(workflow) + if not workflow: + if xmlui.type == "form": + await xmlui.submit_form() + self.host.quit() + + +class List(base.CommandBase): + """List Ad-Hoc commands available on a service""" + + def __init__(self, host): + super(List, self).__init__( + host, "list", use_verbose=True, help=_("list Ad-Hoc commands of a service") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-j", + "--jid", + default="", + help=_("jid of the service (default: profile's server)"), + ) + + async def start(self): + try: + xmlui_raw = await self.host.bridge.ad_hoc_list( + self.args.jid, + self.profile, + ) + except Exception as e: + self.disp(f"can't get ad-hoc commands list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + xmlui = xmlui_manager.create(self.host, xmlui_raw) + await xmlui.show(read_only=True) + self.host.quit() + + +class AdHoc(base.CommandBase): + subcommands = (Run, List, Remote) + + def __init__(self, host): + super(AdHoc, self).__init__( + host, "ad-hoc", use_profile=False, help=_("Ad-hoc commands") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_application.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_application.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from . import base +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp.constants import Const as C + +__commands__ = ["Application"] + + +class List(base.CommandBase): + """List available applications""" + + def __init__(self, host): + super(List, self).__init__( + host, "list", use_profile=False, use_output=C.OUTPUT_LIST, + help=_("list available applications") + ) + + def add_parser_options(self): + # FIXME: "extend" would be better here, but it's only available from Python 3.8+ + # so we use "append" until minimum version of Python is raised. + self.parser.add_argument( + "-f", + "--filter", + dest="filters", + action="append", + choices=["available", "running"], + help=_("show applications with this status"), + ) + + async def start(self): + + # FIXME: this is only needed because we can't use "extend" in + # add_parser_options, see note there + if self.args.filters: + self.args.filters = list(set(self.args.filters)) + else: + self.args.filters = ['available'] + + try: + found_apps = await self.host.bridge.applications_list(self.args.filters) + except Exception as e: + self.disp(f"can't get applications list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(found_apps) + self.host.quit() + + +class Start(base.CommandBase): + """Start an application""" + + def __init__(self, host): + super(Start, self).__init__( + host, "start", use_profile=False, help=_("start an application") + ) + + def add_parser_options(self): + self.parser.add_argument( + "name", + help=_("name of the application to start"), + ) + + async def start(self): + try: + await self.host.bridge.application_start( + self.args.name, + "", + ) + except Exception as e: + self.disp(f"can't start {self.args.name}: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class Stop(base.CommandBase): + + def __init__(self, host): + super(Stop, self).__init__( + host, "stop", use_profile=False, help=_("stop a running application") + ) + + def add_parser_options(self): + id_group = self.parser.add_mutually_exclusive_group(required=True) + id_group.add_argument( + "name", + nargs="?", + help=_("name of the application to stop"), + ) + id_group.add_argument( + "-i", + "--id", + help=_("identifier of the instance to stop"), + ) + + async def start(self): + try: + if self.args.name is not None: + args = [self.args.name, "name"] + else: + args = [self.args.id, "instance"] + await self.host.bridge.application_stop( + *args, + "", + ) + except Exception as e: + if self.args.name is not None: + self.disp( + f"can't stop application {self.args.name!r}: {e}", error=True) + else: + self.disp( + f"can't stop application instance with id {self.args.id!r}: {e}", + error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class Exposed(base.CommandBase): + + def __init__(self, host): + super(Exposed, self).__init__( + host, "exposed", use_profile=False, use_output=C.OUTPUT_DICT, + help=_("show data exposed by a running application") + ) + + def add_parser_options(self): + id_group = self.parser.add_mutually_exclusive_group(required=True) + id_group.add_argument( + "name", + nargs="?", + help=_("name of the application to check"), + ) + id_group.add_argument( + "-i", + "--id", + help=_("identifier of the instance to check"), + ) + + async def start(self): + try: + if self.args.name is not None: + args = [self.args.name, "name"] + else: + args = [self.args.id, "instance"] + exposed_data_raw = await self.host.bridge.application_exposed_get( + *args, + "", + ) + except Exception as e: + if self.args.name is not None: + self.disp( + f"can't get values exposed from application {self.args.name!r}: {e}", + error=True) + else: + self.disp( + f"can't values exposed from application instance with id {self.args.id!r}: {e}", + error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + exposed_data = data_format.deserialise(exposed_data_raw) + await self.output(exposed_data) + self.host.quit() + + +class Application(base.CommandBase): + subcommands = (List, Start, Stop, Exposed) + + def __init__(self, host): + super(Application, self).__init__( + host, "application", use_profile=False, help=_("manage applications"), + aliases=['app'], + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_avatar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_avatar.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import os +import os.path +import asyncio +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.backend.tools import config +from libervia.backend.tools.common import data_format + + +__commands__ = ["Avatar"] +DISPLAY_CMD = ["xdg-open", "xv", "display", "gwenview", "showtell"] + + +class Get(base.CommandBase): + def __init__(self, host): + super(Get, self).__init__( + host, "get", use_verbose=True, help=_("retrieve avatar of an entity") + ) + + def add_parser_options(self): + self.parser.add_argument( + "--no-cache", action="store_true", help=_("do no use cached values") + ) + self.parser.add_argument( + "-s", "--show", action="store_true", help=_("show avatar") + ) + self.parser.add_argument("jid", nargs='?', default='', help=_("entity")) + + async def show_image(self, path): + sat_conf = config.parse_main_conf() + cmd = config.config_get(sat_conf, C.CONFIG_SECTION, "image_cmd") + cmds = [cmd] + DISPLAY_CMD if cmd else DISPLAY_CMD + for cmd in cmds: + try: + process = await asyncio.create_subprocess_exec(cmd, path) + ret = await process.wait() + except OSError: + continue + + if ret in (0, 2): + # we can get exit code 2 with display when stopping it with C-c + break + else: + # didn't worked with commands, we try our luck with webbrowser + # in some cases, webbrowser can actually open the associated display program. + # Note that this may be possibly blocking, depending on the platform and + # available browser + import webbrowser + + webbrowser.open(path) + + async def start(self): + try: + avatar_data_raw = await self.host.bridge.avatar_get( + self.args.jid, + not self.args.no_cache, + self.profile, + ) + except Exception as e: + self.disp(f"can't retrieve avatar: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + avatar_data = data_format.deserialise(avatar_data_raw, type_check=None) + + if not avatar_data: + self.disp(_("No avatar found."), 1) + self.host.quit(C.EXIT_NOT_FOUND) + + avatar_path = avatar_data['path'] + + self.disp(avatar_path) + if self.args.show: + await self.show_image(avatar_path) + + self.host.quit() + + +class Set(base.CommandBase): + def __init__(self, host): + super(Set, self).__init__( + host, "set", use_verbose=True, + help=_("set avatar of the profile or an entity") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-j", "--jid", default='', help=_("entity whose avatar must be changed")) + self.parser.add_argument( + "image_path", type=str, help=_("path to the image to upload") + ) + + async def start(self): + path = self.args.image_path + if not os.path.exists(path): + self.disp(_("file {path} doesn't exist!").format(path=repr(path)), error=True) + self.host.quit(C.EXIT_BAD_ARG) + path = os.path.abspath(path) + try: + await self.host.bridge.avatar_set(path, self.args.jid, self.profile) + except Exception as e: + self.disp(f"can't set avatar: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("avatar has been set"), 1) + self.host.quit() + + +class Avatar(base.CommandBase): + subcommands = (Get, Set) + + def __init__(self, host): + super(Avatar, self).__init__( + host, "avatar", use_profile=False, help=_("avatar uploading/retrieving") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_blocking.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_blocking.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import json +import os +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp import common +from libervia.frontends.jp.constants import Const as C +from . import base + +__commands__ = ["Blocking"] + + +class List(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "list", + use_output=C.OUTPUT_LIST, + help=_("list blocked entities"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + blocked_jids = await self.host.bridge.blocking_list( + self.profile, + ) + except Exception as e: + self.disp(f"can't get blocked entities: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(blocked_jids) + self.host.quit(C.EXIT_OK) + + +class Block(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "block", + help=_("block one or more entities"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "entities", + nargs="+", + metavar="JID", + help=_("JIDs of entities to block"), + ) + + async def start(self): + try: + await self.host.bridge.blocking_block( + self.args.entities, + self.profile + ) + except Exception as e: + self.disp(f"can't block entities: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit(C.EXIT_OK) + + +class Unblock(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "unblock", + help=_("unblock one or more entities"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "entities", + nargs="+", + metavar="JID", + help=_("JIDs of entities to unblock"), + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_('when "all" is used, unblock all entities without confirmation'), + ) + + async def start(self): + if self.args.entities == ["all"]: + if not self.args.force: + await self.host.confirm_or_quit( + _("All entities will be unblocked, are you sure"), + _("unblock cancelled") + ) + self.args.entities.clear() + elif self.args.force: + self.parser.error(_('--force is only allowed when "all" is used as target')) + + try: + await self.host.bridge.blocking_unblock( + self.args.entities, + self.profile + ) + except Exception as e: + self.disp(f"can't unblock entities: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit(C.EXIT_OK) + + +class Blocking(base.CommandBase): + subcommands = (List, Block, Unblock) + + def __init__(self, host): + super().__init__( + host, "blocking", use_profile=False, help=_("entities blocking") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_blog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_blog.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1219 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import asyncio +from asyncio.subprocess import DEVNULL +from configparser import NoOptionError, NoSectionError +import json +import os +import os.path +from pathlib import Path +import re +import subprocess +import sys +import tempfile +from urllib.parse import urlparse + +from libervia.backend.core.i18n import _ +from libervia.backend.tools import config +from libervia.backend.tools.common import uri +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.frontends.jp import common +from libervia.frontends.jp.constants import Const as C + +from . import base, cmd_pubsub + +__commands__ = ["Blog"] + +SYNTAX_XHTML = "xhtml" +# extensions to use with known syntaxes +SYNTAX_EXT = { + # FIXME: default syntax doesn't sounds needed, there should always be a syntax set + # by the plugin. + "": "txt", # used when the syntax is not found + SYNTAX_XHTML: "xhtml", + "markdown": "md", +} + + +CONF_SYNTAX_EXT = "syntax_ext_dict" +BLOG_TMP_DIR = "blog" +# key to remove from metadata tmp file if they exist +KEY_TO_REMOVE_METADATA = ( + "id", + "content", + "content_xhtml", + "comments_node", + "comments_service", + "updated", +) + +URL_REDIRECT_PREFIX = "url_redirect_" +AIONOTIFY_INSTALL = '"pip install aionotify"' +MB_KEYS = ( + "id", + "url", + "atom_id", + "updated", + "published", + "language", + "comments", # this key is used for all comments* keys + "tags", # this key is used for all tag* keys + "author", + "author_jid", + "author_email", + "author_jid_verified", + "content", + "content_xhtml", + "title", + "title_xhtml", + "extra" +) +OUTPUT_OPT_NO_HEADER = "no-header" +RE_ATTACHMENT_METADATA = re.compile(r"^(?P[a-z_]+)=(?P.*)") +ALLOWER_ATTACH_MD_KEY = ("desc", "media_type", "external") + + +async def guess_syntax_from_path(host, sat_conf, path): + """Return syntax guessed according to filename extension + + @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration + @param path(str): path to the content file + @return(unicode): syntax to use + """ + # we first try to guess syntax with extension + ext = os.path.splitext(path)[1][1:] # we get extension without the '.' + if ext: + for k, v in SYNTAX_EXT.items(): + if k and ext == v: + return k + + # if not found, we use current syntax + return await host.bridge.param_get_a("Syntax", "Composition", "value", host.profile) + + +class BlogPublishCommon: + """handle common option for publising commands (Set and Edit)""" + + async def get_current_syntax(self): + """Retrieve current_syntax + + Use default syntax if --syntax has not been used, else check given syntax. + Will set self.default_syntax_used to True if default syntax has been used + """ + if self.args.syntax is None: + self.default_syntax_used = True + return await self.host.bridge.param_get_a( + "Syntax", "Composition", "value", self.profile + ) + else: + self.default_syntax_used = False + try: + syntax = await self.host.bridge.syntax_get(self.args.syntax) + self.current_syntax = self.args.syntax = syntax + except Exception as e: + if e.classname == "NotFound": + self.parser.error( + _("unknown syntax requested ({syntax})").format( + syntax=self.args.syntax + ) + ) + else: + raise e + return self.args.syntax + + def add_parser_options(self): + self.parser.add_argument("-T", "--title", help=_("title of the item")) + self.parser.add_argument( + "-t", + "--tag", + action="append", + help=_("tag (category) of your item"), + ) + self.parser.add_argument( + "-l", + "--language", + help=_("language of the item (ISO 639 code)"), + ) + + self.parser.add_argument( + "-a", + "--attachment", + dest="attachments", + nargs="+", + help=_( + "attachment in the form URL [metadata_name=value]" + ) + ) + + comments_group = self.parser.add_mutually_exclusive_group() + comments_group.add_argument( + "-C", + "--comments", + action="store_const", + const=True, + dest="comments", + help=_( + "enable comments (default: comments not enabled except if they " + "already exist)" + ), + ) + comments_group.add_argument( + "--no-comments", + action="store_const", + const=False, + dest="comments", + help=_("disable comments (will remove comments node if it exist)"), + ) + + self.parser.add_argument( + "-S", + "--syntax", + help=_("syntax to use (default: get profile's default syntax)"), + ) + self.parser.add_argument( + "-e", + "--encrypt", + action="store_true", + help=_("end-to-end encrypt the blog post") + ) + self.parser.add_argument( + "--encrypt-for", + metavar="JID", + action="append", + help=_("encrypt a single item for") + ) + self.parser.add_argument( + "-X", + "--sign", + action="store_true", + help=_("cryptographically sign the blog post") + ) + + async def set_mb_data_content(self, content, mb_data): + if self.default_syntax_used: + # default syntax has been used + mb_data["content_rich"] = content + elif self.current_syntax == SYNTAX_XHTML: + mb_data["content_xhtml"] = content + else: + mb_data["content_xhtml"] = await self.host.bridge.syntax_convert( + content, self.current_syntax, SYNTAX_XHTML, False, self.profile + ) + + def handle_attachments(self, mb_data: dict) -> None: + """Check, validate and add attachments to mb_data""" + if self.args.attachments: + attachments = [] + attachment = {} + for arg in self.args.attachments: + m = RE_ATTACHMENT_METADATA.match(arg) + if m is None: + # we should have an URL + url_parsed = urlparse(arg) + if url_parsed.scheme not in ("http", "https"): + self.parser.error( + "invalid URL in --attachment (only http(s) scheme is " + f" accepted): {arg}" + ) + if attachment: + # if we hae a new URL, we have a new attachment + attachments.append(attachment) + attachment = {} + attachment["url"] = arg + else: + # we should have a metadata + if "url" not in attachment: + self.parser.error( + "you must to specify an URL before any metadata in " + "--attachment" + ) + key = m.group("key") + if key not in ALLOWER_ATTACH_MD_KEY: + self.parser.error( + f"invalid metadata key in --attachment: {key!r}" + ) + value = m.group("value").strip() + if key == "external": + if not value: + value=True + else: + value = C.bool(value) + attachment[key] = value + if attachment: + attachments.append(attachment) + if attachments: + mb_data.setdefault("extra", {})["attachments"] = attachments + + def set_mb_data_from_args(self, mb_data): + """set microblog metadata according to command line options + + if metadata already exist, it will be overwritten + """ + if self.args.comments is not None: + mb_data["allow_comments"] = self.args.comments + if self.args.tag: + mb_data["tags"] = self.args.tag + if self.args.title is not None: + mb_data["title"] = self.args.title + if self.args.language is not None: + mb_data["language"] = self.args.language + if self.args.encrypt: + mb_data["encrypted"] = True + if self.args.sign: + mb_data["signed"] = True + if self.args.encrypt_for: + mb_data["encrypted_for"] = {"targets": self.args.encrypt_for} + self.handle_attachments(mb_data) + + +class Set(base.CommandBase, BlogPublishCommon): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + help=_("publish a new blog item or update an existing one"), + ) + BlogPublishCommon.__init__(self) + + def add_parser_options(self): + BlogPublishCommon.add_parser_options(self) + + async def start(self): + self.current_syntax = await self.get_current_syntax() + self.pubsub_item = self.args.item + mb_data = {} + self.set_mb_data_from_args(mb_data) + if self.pubsub_item: + mb_data["id"] = self.pubsub_item + content = sys.stdin.read() + await self.set_mb_data_content(content, mb_data) + + try: + item_id = await self.host.bridge.mb_send( + self.args.service, + self.args.node, + data_format.serialise(mb_data), + self.profile, + ) + except Exception as e: + self.disp(f"can't send item: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(f"Item published with ID {item_id}") + self.host.quit(C.EXIT_OK) + + +class Get(base.CommandBase): + TEMPLATE = "blog/articles.html" + + def __init__(self, host): + extra_outputs = {"default": self.default_output, "fancy": self.fancy_output} + base.CommandBase.__init__( + self, + host, + "get", + use_verbose=True, + use_pubsub=True, + pubsub_flags={C.MULTI_ITEMS, C.CACHE}, + use_output=C.OUTPUT_COMPLEX, + extra_outputs=extra_outputs, + help=_("get blog item(s)"), + ) + + def add_parser_options(self): + #  TODO: a key(s) argument to select keys to display + self.parser.add_argument( + "-k", + "--key", + action="append", + dest="keys", + help=_("microblog data key(s) to display (default: depend of verbosity)"), + ) + # TODO: add MAM filters + + def template_data_mapping(self, data): + items, blog_items = data + blog_items["items"] = items + return {"blog_items": blog_items} + + def format_comments(self, item, keys): + lines = [] + for data in item.get("comments", []): + lines.append(data["uri"]) + for k in ("node", "service"): + if OUTPUT_OPT_NO_HEADER in self.args.output_opts: + header = "" + else: + header = f"{C.A_HEADER}comments_{k}: {A.RESET}" + lines.append(header + data[k]) + return "\n".join(lines) + + def format_tags(self, item, keys): + tags = item.pop("tags", []) + return ", ".join(tags) + + def format_updated(self, item, keys): + return common.format_time(item["updated"]) + + def format_published(self, item, keys): + return common.format_time(item["published"]) + + def format_url(self, item, keys): + return uri.build_xmpp_uri( + "pubsub", + subtype="microblog", + path=self.metadata["service"], + node=self.metadata["node"], + item=item["id"], + ) + + def get_keys(self): + """return keys to display according to verbosity or explicit key request""" + verbosity = self.args.verbose + if self.args.keys: + if not set(MB_KEYS).issuperset(self.args.keys): + self.disp( + "following keys are invalid: {invalid}.\n" + "Valid keys are: {valid}.".format( + invalid=", ".join(set(self.args.keys).difference(MB_KEYS)), + valid=", ".join(sorted(MB_KEYS)), + ), + error=True, + ) + self.host.quit(C.EXIT_BAD_ARG) + return self.args.keys + else: + if verbosity == 0: + return ("title", "content") + elif verbosity == 1: + return ( + "title", + "tags", + "author", + "author_jid", + "author_email", + "author_jid_verified", + "published", + "updated", + "content", + ) + else: + return MB_KEYS + + def default_output(self, data): + """simple key/value output""" + items, self.metadata = data + keys = self.get_keys() + + #  k_cb use format_[key] methods for complex formattings + k_cb = {} + for k in keys: + try: + callback = getattr(self, "format_" + k) + except AttributeError: + pass + else: + k_cb[k] = callback + for idx, item in enumerate(items): + for k in keys: + if k not in item and k not in k_cb: + continue + if OUTPUT_OPT_NO_HEADER in self.args.output_opts: + header = "" + else: + header = "{k_fmt}{key}:{k_fmt_e} {sep}".format( + k_fmt=C.A_HEADER, + key=k, + k_fmt_e=A.RESET, + sep="\n" if "content" in k else "", + ) + value = k_cb[k](item, keys) if k in k_cb else item[k] + if isinstance(value, bool): + value = str(value).lower() + elif isinstance(value, dict): + value = repr(value) + self.disp(header + (value or "")) + # we want a separation line after each item but the last one + if idx < len(items) - 1: + print("") + + def fancy_output(self, data): + """display blog is a nice to read way + + this output doesn't use keys filter + """ + # thanks to http://stackoverflow.com/a/943921 + rows, columns = list(map(int, os.popen("stty size", "r").read().split())) + items, metadata = data + verbosity = self.args.verbose + sep = A.color(A.FG_BLUE, columns * "▬") + if items: + print(("\n" + sep + "\n")) + + for idx, item in enumerate(items): + title = item.get("title") + if verbosity > 0: + author = item["author"] + published, updated = item["published"], item.get("updated") + else: + author = published = updated = None + if verbosity > 1: + tags = item.pop("tags", []) + else: + tags = None + content = item.get("content") + + if title: + print((A.color(A.BOLD, A.FG_CYAN, item["title"]))) + meta = [] + if author: + meta.append(A.color(A.FG_YELLOW, author)) + if published: + meta.append(A.color(A.FG_YELLOW, "on ", common.format_time(published))) + if updated != published: + meta.append( + A.color(A.FG_YELLOW, "(updated on ", common.format_time(updated), ")") + ) + print((" ".join(meta))) + if tags: + print((A.color(A.FG_MAGENTA, ", ".join(tags)))) + if (title or tags) and content: + print("") + if content: + self.disp(content) + + print(("\n" + sep + "\n")) + + async def start(self): + try: + mb_data = data_format.deserialise( + await self.host.bridge.mb_get( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + self.get_pubsub_extra(), + self.profile, + ) + ) + except Exception as e: + self.disp(f"can't get blog items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + items = mb_data.pop("items") + await self.output((items, mb_data)) + self.host.quit(C.EXIT_OK) + + +class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "edit", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + use_draft=True, + use_verbose=True, + help=_("edit an existing or new blog post"), + ) + BlogPublishCommon.__init__(self) + common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) + + def add_parser_options(self): + BlogPublishCommon.add_parser_options(self) + self.parser.add_argument( + "-P", + "--preview", + action="store_true", + help=_("launch a blog preview in parallel"), + ) + self.parser.add_argument( + "--no-publish", + action="store_true", + help=_('add "publish: False" to metadata'), + ) + + def build_metadata_file(self, content_file_path, mb_data=None): + """Build a metadata file using json + + The file is named after content_file_path, with extension replaced by + _metadata.json + @param content_file_path(str): path to the temporary file which will contain the + body + @param mb_data(dict, None): microblog metadata (for existing items) + @return (tuple[dict, Path]): merged metadata put originaly in metadata file + and path to temporary metadata file + """ + # we first construct metadata from edited item ones and CLI argumments + # or re-use the existing one if it exists + meta_file_path = content_file_path.with_name( + content_file_path.stem + common.METADATA_SUFF + ) + if meta_file_path.exists(): + self.disp("Metadata file already exists, we re-use it") + try: + with meta_file_path.open("rb") as f: + mb_data = json.load(f) + except (OSError, IOError, ValueError) as e: + self.disp( + f"Can't read existing metadata file at {meta_file_path}, " + f"aborting: {e}", + error=True, + ) + self.host.quit(1) + else: + mb_data = {} if mb_data is None else mb_data.copy() + + # in all cases, we want to remove unwanted keys + for key in KEY_TO_REMOVE_METADATA: + try: + del mb_data[key] + except KeyError: + pass + # and override metadata with command-line arguments + self.set_mb_data_from_args(mb_data) + + if self.args.no_publish: + mb_data["publish"] = False + + # then we create the file and write metadata there, as JSON dict + # XXX: if we port jp one day on Windows, O_BINARY may need to be added here + with os.fdopen( + os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b" + ) as f: + # we need to use an intermediate unicode buffer to write to the file + # unicode without escaping characters + unicode_dump = json.dumps( + mb_data, + ensure_ascii=False, + indent=4, + separators=(",", ": "), + sort_keys=True, + ) + f.write(unicode_dump.encode("utf-8")) + + return mb_data, meta_file_path + + async def edit(self, content_file_path, content_file_obj, mb_data=None): + """Edit the file contening the content using editor, and publish it""" + # we first create metadata file + meta_ori, meta_file_path = self.build_metadata_file(content_file_path, mb_data) + + coroutines = [] + + # do we need a preview ? + if self.args.preview: + self.disp("Preview requested, launching it", 1) + # we redirect outputs to /dev/null to avoid console pollution in editor + # if user wants to see messages, (s)he can call "blog preview" directly + coroutines.append( + asyncio.create_subprocess_exec( + sys.argv[0], + "blog", + "preview", + "--inotify", + "true", + "-p", + self.profile, + str(content_file_path), + stdout=DEVNULL, + stderr=DEVNULL, + ) + ) + + # we launch editor + coroutines.append( + self.run_editor( + "blog_editor_args", + content_file_path, + content_file_obj, + meta_file_path=meta_file_path, + meta_ori=meta_ori, + ) + ) + + await asyncio.gather(*coroutines) + + async def publish(self, content, mb_data): + await self.set_mb_data_content(content, mb_data) + + if self.pubsub_item: + mb_data["id"] = self.pubsub_item + + mb_data = data_format.serialise(mb_data) + + await self.host.bridge.mb_send( + self.pubsub_service, self.pubsub_node, mb_data, self.profile + ) + self.disp("Blog item published") + + def get_tmp_suff(self): + # we get current syntax to determine file extension + return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""]) + + async def get_item_data(self, service, node, item): + items = [item] if item else [] + + mb_data = data_format.deserialise( + await self.host.bridge.mb_get( + service, node, 1, items, data_format.serialise({}), self.profile + ) + ) + item = mb_data["items"][0] + + try: + content = item["content_xhtml"] + except KeyError: + content = item["content"] + if content: + content = await self.host.bridge.syntax_convert( + content, "text", SYNTAX_XHTML, False, self.profile + ) + + if content and self.current_syntax != SYNTAX_XHTML: + content = await self.host.bridge.syntax_convert( + content, SYNTAX_XHTML, self.current_syntax, False, self.profile + ) + + if content and self.current_syntax == SYNTAX_XHTML: + content = content.strip() + if not content.startswith("
"): + content = "
" + content + "
" + try: + from lxml import etree + except ImportError: + self.disp(_("You need lxml to edit pretty XHTML")) + else: + parser = etree.XMLParser(remove_blank_text=True) + root = etree.fromstring(content, parser) + content = etree.tostring(root, encoding=str, pretty_print=True) + + return content, item, item["id"] + + async def start(self): + # if there are user defined extension, we use them + SYNTAX_EXT.update( + config.config_get(self.sat_conf, C.CONFIG_SECTION, CONF_SYNTAX_EXT, {}) + ) + self.current_syntax = await self.get_current_syntax() + + ( + self.pubsub_service, + self.pubsub_node, + self.pubsub_item, + content_file_path, + content_file_obj, + mb_data, + ) = await self.get_item_path() + + await self.edit(content_file_path, content_file_obj, mb_data=mb_data) + self.host.quit() + + +class Rename(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "rename", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + help=_("rename an blog item"), + ) + + def add_parser_options(self): + self.parser.add_argument("new_id", help=_("new item id to use")) + + async def start(self): + try: + await self.host.bridge.mb_rename( + self.args.service, + self.args.node, + self.args.item, + self.args.new_id, + self.profile, + ) + except Exception as e: + self.disp(f"can't rename item: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp("Item renamed") + self.host.quit(C.EXIT_OK) + + +class Repeat(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "repeat", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + help=_("repeat (re-publish) a blog item"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + repeat_id = await self.host.bridge.mb_repeat( + self.args.service, + self.args.node, + self.args.item, + "", + self.profile, + ) + except Exception as e: + self.disp(f"can't repeat item: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if repeat_id: + self.disp(f"Item repeated at ID {str(repeat_id)!r}") + else: + self.disp("Item repeated") + self.host.quit(C.EXIT_OK) + + +class Preview(base.CommandBase, common.BaseEdit): + # TODO: need to be rewritten with template output + + def __init__(self, host): + base.CommandBase.__init__( + self, host, "preview", use_verbose=True, help=_("preview a blog content") + ) + common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) + + def add_parser_options(self): + self.parser.add_argument( + "--inotify", + type=str, + choices=("auto", "true", "false"), + default="auto", + help=_("use inotify to handle preview"), + ) + self.parser.add_argument( + "file", + nargs="?", + default="current", + help=_("path to the content file"), + ) + + async def show_preview(self): + # we implement show_preview here so we don't have to import webbrowser and urllib + # when preview is not used + url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) + self.webbrowser.open_new_tab(url) + + async def _launch_preview_ext(self, cmd_line, opt_name): + url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) + args = common.parse_args( + self.host, cmd_line, url=url, preview_file=self.preview_file_path + ) + if not args: + self.disp( + 'Couln\'t find command in "{name}", abording'.format(name=opt_name), + error=True, + ) + self.host.quit(1) + subprocess.Popen(args) + + async def open_preview_ext(self): + await self._launch_preview_ext(self.open_cb_cmd, "blog_preview_open_cmd") + + async def update_preview_ext(self): + await self._launch_preview_ext(self.update_cb_cmd, "blog_preview_update_cmd") + + async def update_content(self): + with self.content_file_path.open("rb") as f: + content = f.read().decode("utf-8-sig") + if content and self.syntax != SYNTAX_XHTML: + # we use safe=True because we want to have a preview as close as possible + # to what the people will see + content = await self.host.bridge.syntax_convert( + content, self.syntax, SYNTAX_XHTML, True, self.profile + ) + + xhtml = ( + f'' + f'' + f"" + f"{content}" + f"" + ) + + with open(self.preview_file_path, "wb") as f: + f.write(xhtml.encode("utf-8")) + + async def start(self): + import webbrowser + import urllib.request, urllib.parse, urllib.error + + self.webbrowser, self.urllib = webbrowser, urllib + + if self.args.inotify != "false": + try: + import aionotify + + except ImportError: + if self.args.inotify == "auto": + aionotify = None + self.disp( + f"aionotify module not found, deactivating feature. You can " + f"install it with {AIONOTIFY_INSTALL}" + ) + else: + self.disp( + f"aioinotify not found, can't activate the feature! Please " + f"install it with {AIONOTIFY_INSTALL}", + error=True, + ) + self.host.quit(1) + else: + aionotify = None + + sat_conf = self.sat_conf + SYNTAX_EXT.update( + config.config_get(sat_conf, C.CONFIG_SECTION, CONF_SYNTAX_EXT, {}) + ) + + try: + self.open_cb_cmd = config.config_get( + sat_conf, C.CONFIG_SECTION, "blog_preview_open_cmd", Exception + ) + except (NoOptionError, NoSectionError): + self.open_cb_cmd = None + open_cb = self.show_preview + else: + open_cb = self.open_preview_ext + + self.update_cb_cmd = config.config_get( + sat_conf, C.CONFIG_SECTION, "blog_preview_update_cmd", self.open_cb_cmd + ) + if self.update_cb_cmd is None: + update_cb = self.show_preview + else: + update_cb = self.update_preview_ext + + # which file do we need to edit? + if self.args.file == "current": + self.content_file_path = self.get_current_file(self.profile) + else: + try: + self.content_file_path = Path(self.args.file).resolve(strict=True) + except FileNotFoundError: + self.disp(_('File "{file}" doesn\'t exist!').format(file=self.args.file)) + self.host.quit(C.EXIT_NOT_FOUND) + + self.syntax = await guess_syntax_from_path( + self.host, sat_conf, self.content_file_path + ) + + # at this point the syntax is converted, we can display the preview + preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False) + self.preview_file_path = preview_file.name + preview_file.close() + await self.update_content() + + if aionotify is None: + # XXX: we don't delete file automatically because browser needs it + # (and webbrowser.open can return before it is read) + self.disp( + f"temporary file created at {self.preview_file_path}\nthis file will NOT " + f"BE DELETED AUTOMATICALLY, please delete it yourself when you have " + f"finished" + ) + await open_cb() + else: + await open_cb() + watcher = aionotify.Watcher() + watcher_kwargs = { + # Watcher don't accept Path so we convert to string + "path": str(self.content_file_path), + "alias": "content_file", + "flags": aionotify.Flags.CLOSE_WRITE + | aionotify.Flags.DELETE_SELF + | aionotify.Flags.MOVE_SELF, + } + watcher.watch(**watcher_kwargs) + + loop = asyncio.get_event_loop() + await watcher.setup(loop) + + try: + while True: + event = await watcher.get_event() + self.disp("Content updated", 1) + if event.flags & ( + aionotify.Flags.DELETE_SELF | aionotify.Flags.MOVE_SELF + ): + self.disp( + "DELETE/MOVE event catched, changing the watch", + 2, + ) + try: + watcher.unwatch("content_file") + except IOError as e: + self.disp( + f"Can't remove the watch: {e}", + 2, + ) + watcher = aionotify.Watcher() + watcher.watch(**watcher_kwargs) + try: + await watcher.setup(loop) + except OSError: + # if the new file is not here yet we can have an error + # as a workaround, we do a little rest and try again + await asyncio.sleep(1) + await watcher.setup(loop) + await self.update_content() + await update_cb() + except FileNotFoundError: + self.disp("The file seems to have been deleted.", error=True) + self.host.quit(C.EXIT_NOT_FOUND) + finally: + os.unlink(self.preview_file_path) + try: + watcher.unwatch("content_file") + except IOError as e: + self.disp( + f"Can't remove the watch: {e}", + 2, + ) + + +class Import(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "import", + use_pubsub=True, + use_progress=True, + help=_("import an external blog"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "importer", + nargs="?", + help=_("importer name, nothing to display importers list"), + ) + self.parser.add_argument("--host", help=_("original blog host")) + self.parser.add_argument( + "--no-images-upload", + action="store_true", + help=_("do *NOT* upload images (default: do upload images)"), + ) + self.parser.add_argument( + "--upload-ignore-host", + help=_("do not upload images from this host (default: upload all images)"), + ) + self.parser.add_argument( + "--ignore-tls-errors", + action="store_true", + help=_("ignore invalide TLS certificate for uploads"), + ) + self.parser.add_argument( + "-o", + "--option", + action="append", + nargs=2, + default=[], + metavar=("NAME", "VALUE"), + help=_("importer specific options (see importer description)"), + ) + self.parser.add_argument( + "location", + nargs="?", + help=_( + "importer data location (see importer description), nothing to show " + "importer description" + ), + ) + + async def on_progress_started(self, metadata): + self.disp(_("Blog upload started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("Blog uploaded successfully"), 2) + redirections = { + k[len(URL_REDIRECT_PREFIX) :]: v + for k, v in metadata.items() + if k.startswith(URL_REDIRECT_PREFIX) + } + if redirections: + conf = "\n".join( + [ + "url_redirections_dict = {}".format( + # we need to add ' ' before each new line + # and to double each '%' for ConfigParser + "\n ".join( + json.dumps(redirections, indent=1, separators=(",", ": ")) + .replace("%", "%%") + .split("\n") + ) + ), + ] + ) + self.disp( + _( + "\nTo redirect old URLs to new ones, put the following lines in your" + " sat.conf file, in [libervia] section:\n\n{conf}" + ).format(conf=conf) + ) + + async def on_progress_error(self, error_msg): + self.disp( + _("Error while uploading blog: {error_msg}").format(error_msg=error_msg), + error=True, + ) + + async def start(self): + if self.args.location is None: + for name in ("option", "service", "no_images_upload"): + if getattr(self.args, name): + self.parser.error( + _( + "{name} argument can't be used without location argument" + ).format(name=name) + ) + if self.args.importer is None: + self.disp( + "\n".join( + [ + f"{name}: {desc}" + for name, desc in await self.host.bridge.blogImportList() + ] + ) + ) + else: + try: + short_desc, long_desc = await self.host.bridge.blogImportDesc( + self.args.importer + ) + except Exception as e: + msg = [l for l in str(e).split("\n") if l][ + -1 + ] # we only keep the last line + self.disp(msg) + self.host.quit(1) + else: + self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}") + self.host.quit() + else: + # we have a location, an import is requested + options = {key: value for key, value in self.args.option} + if self.args.host: + options["host"] = self.args.host + if self.args.ignore_tls_errors: + options["ignore_tls_errors"] = C.BOOL_TRUE + if self.args.no_images_upload: + options["upload_images"] = C.BOOL_FALSE + if self.args.upload_ignore_host: + self.parser.error( + "upload-ignore-host option can't be used when no-images-upload " + "is set" + ) + elif self.args.upload_ignore_host: + options["upload_ignore_host"] = self.args.upload_ignore_host + + try: + progress_id = await self.host.bridge.blogImport( + self.args.importer, + self.args.location, + options, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp( + _("Error while trying to import a blog: {e}").format(e=e), + error=True, + ) + self.host.quit(1) + else: + await self.set_progress_id(progress_id) + + +class AttachmentGet(cmd_pubsub.AttachmentGet): + + def __init__(self, host): + super().__init__(host) + self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM}) + + + async def start(self): + if not self.args.node: + namespaces = await self.host.bridge.namespaces_get() + try: + ns_microblog = namespaces["microblog"] + except KeyError: + self.disp("XEP-0277 plugin is not loaded", error=True) + self.host.quit(C.EXIT_MISSING_FEATURE) + else: + self.args.node = ns_microblog + return await super().start() + + +class AttachmentSet(cmd_pubsub.AttachmentSet): + + def __init__(self, host): + super().__init__(host) + self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM}) + + async def start(self): + if not self.args.node: + namespaces = await self.host.bridge.namespaces_get() + try: + ns_microblog = namespaces["microblog"] + except KeyError: + self.disp("XEP-0277 plugin is not loaded", error=True) + self.host.quit(C.EXIT_MISSING_FEATURE) + else: + self.args.node = ns_microblog + return await super().start() + + +class Attachments(base.CommandBase): + subcommands = (AttachmentGet, AttachmentSet) + + def __init__(self, host): + super().__init__( + host, + "attachments", + use_profile=False, + help=_("set or retrieve blog attachments"), + ) + + +class Blog(base.CommandBase): + subcommands = (Set, Get, Edit, Rename, Repeat, Preview, Import, Attachments) + + def __init__(self, host): + super(Blog, self).__init__( + host, "blog", use_profile=False, help=_("blog/microblog management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_bookmarks.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_bookmarks.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C + +__commands__ = ["Bookmarks"] + +STORAGE_LOCATIONS = ("local", "private", "pubsub") +TYPES = ("muc", "url") + + +class BookmarksCommon(base.CommandBase): + """Class used to group common options of bookmarks subcommands""" + + def add_parser_options(self, location_default="all"): + self.parser.add_argument( + "-l", + "--location", + type=str, + choices=(location_default,) + STORAGE_LOCATIONS, + default=location_default, + help=_("storage location (default: %(default)s)"), + ) + self.parser.add_argument( + "-t", + "--type", + type=str, + choices=TYPES, + default=TYPES[0], + help=_("bookmarks type (default: %(default)s)"), + ) + + +class BookmarksList(BookmarksCommon): + def __init__(self, host): + super(BookmarksList, self).__init__(host, "list", help=_("list bookmarks")) + + async def start(self): + try: + data = await self.host.bridge.bookmarks_list( + self.args.type, self.args.location, self.host.profile + ) + except Exception as e: + self.disp(f"can't get bookmarks list: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + mess = [] + for location in STORAGE_LOCATIONS: + if not data[location]: + continue + loc_mess = [] + loc_mess.append(f"{location}:") + book_mess = [] + for book_link, book_data in list(data[location].items()): + name = book_data.get("name") + autojoin = book_data.get("autojoin", "false") == "true" + nick = book_data.get("nick") + book_mess.append( + "\t%s[%s%s]%s" + % ( + (name + " ") if name else "", + book_link, + " (%s)" % nick if nick else "", + " (*)" if autojoin else "", + ) + ) + loc_mess.append("\n".join(book_mess)) + mess.append("\n".join(loc_mess)) + + print("\n\n".join(mess)) + self.host.quit() + + +class BookmarksRemove(BookmarksCommon): + def __init__(self, host): + super(BookmarksRemove, self).__init__(host, "remove", help=_("remove a bookmark")) + + def add_parser_options(self): + super(BookmarksRemove, self).add_parser_options() + self.parser.add_argument( + "bookmark", help=_("jid (for muc bookmark) or url of to remove") + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("delete bookmark without confirmation"), + ) + + async def start(self): + if not self.args.force: + await self.host.confirm_or_quit(_("Are you sure to delete this bookmark?")) + + try: + await self.host.bridge.bookmarks_remove( + self.args.type, self.args.bookmark, self.args.location, self.host.profile + ) + except Exception as e: + self.disp(_("can't delete bookmark: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("bookmark deleted")) + self.host.quit() + + +class BookmarksAdd(BookmarksCommon): + def __init__(self, host): + super(BookmarksAdd, self).__init__(host, "add", help=_("add a bookmark")) + + def add_parser_options(self): + super(BookmarksAdd, self).add_parser_options(location_default="auto") + self.parser.add_argument( + "bookmark", help=_("jid (for muc bookmark) or url of to remove") + ) + self.parser.add_argument("-n", "--name", help=_("bookmark name")) + muc_group = self.parser.add_argument_group(_("MUC specific options")) + muc_group.add_argument("-N", "--nick", help=_("nickname")) + muc_group.add_argument( + "-a", + "--autojoin", + action="store_true", + help=_("join room on profile connection"), + ) + + async def start(self): + if self.args.type == "url" and (self.args.autojoin or self.args.nick is not None): + self.parser.error(_("You can't use --autojoin or --nick with --type url")) + data = {} + if self.args.autojoin: + data["autojoin"] = "true" + if self.args.nick is not None: + data["nick"] = self.args.nick + if self.args.name is not None: + data["name"] = self.args.name + try: + await self.host.bridge.bookmarks_add( + self.args.type, + self.args.bookmark, + data, + self.args.location, + self.host.profile, + ) + except Exception as e: + self.disp(f"can't add bookmark: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("bookmark successfully added")) + self.host.quit() + + +class Bookmarks(base.CommandBase): + subcommands = (BookmarksList, BookmarksRemove, BookmarksAdd) + + def __init__(self, host): + super(Bookmarks, self).__init__( + host, "bookmarks", use_profile=False, help=_("manage bookmarks") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_debug.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_debug.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.backend.tools.common.ansi import ANSI as A +import json + +__commands__ = ["Debug"] + + +class BridgeCommon(object): + def eval_args(self): + if self.args.arg: + try: + return eval("[{}]".format(",".join(self.args.arg))) + except SyntaxError as e: + self.disp( + "Can't evaluate arguments: {mess}\n{text}\n{offset}^".format( + mess=e, text=e.text, offset=" " * (e.offset - 1) + ), + error=True, + ) + self.host.quit(C.EXIT_BAD_ARG) + else: + return [] + + +class Method(base.CommandBase, BridgeCommon): + def __init__(self, host): + base.CommandBase.__init__(self, host, "method", help=_("call a bridge method")) + BridgeCommon.__init__(self) + + def add_parser_options(self): + self.parser.add_argument( + "method", type=str, help=_("name of the method to execute") + ) + self.parser.add_argument("arg", nargs="*", help=_("argument of the method")) + + async def start(self): + method = getattr(self.host.bridge, self.args.method) + import inspect + + argspec = inspect.getargspec(method) + + kwargs = {} + if "profile_key" in argspec.args: + kwargs["profile_key"] = self.profile + elif "profile" in argspec.args: + kwargs["profile"] = self.profile + + args = self.eval_args() + + try: + ret = await method( + *args, + **kwargs, + ) + except Exception as e: + self.disp( + _("Error while executing {method}: {e}").format( + method=self.args.method, e=e + ), + error=True, + ) + self.host.quit(C.EXIT_ERROR) + else: + if ret is not None: + self.disp(str(ret)) + self.host.quit() + + +class Signal(base.CommandBase, BridgeCommon): + def __init__(self, host): + base.CommandBase.__init__( + self, host, "signal", help=_("send a fake signal from backend") + ) + BridgeCommon.__init__(self) + + def add_parser_options(self): + self.parser.add_argument("signal", type=str, help=_("name of the signal to send")) + self.parser.add_argument("arg", nargs="*", help=_("argument of the signal")) + + async def start(self): + args = self.eval_args() + json_args = json.dumps(args) + # XXX: we use self.args.profile and not self.profile + # because we want the raw profile_key (so plugin handle C.PROF_KEY_NONE) + try: + await self.host.bridge.debug_signal_fake( + self.args.signal, json_args, self.args.profile + ) + except Exception as e: + self.disp(_("Can't send fake signal: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_ERROR) + else: + self.host.quit() + + +class bridge(base.CommandBase): + subcommands = (Method, Signal) + + def __init__(self, host): + super(bridge, self).__init__( + host, "bridge", use_profile=False, help=_("bridge s(t)imulation") + ) + + +class Monitor(base.CommandBase): + def __init__(self, host): + super(Monitor, self).__init__( + host, + "monitor", + use_verbose=True, + use_profile=False, + use_output=C.OUTPUT_XML, + help=_("monitor XML stream"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-d", + "--direction", + choices=("in", "out", "both"), + default="both", + help=_("stream direction filter"), + ) + + async def print_xml(self, direction, xml_data, profile): + if self.args.direction == "in" and direction != "IN": + return + if self.args.direction == "out" and direction != "OUT": + return + verbosity = self.host.verbosity + if not xml_data.strip(): + if verbosity <= 2: + return + whiteping = True + else: + whiteping = False + + if verbosity: + profile_disp = f" ({profile})" if verbosity > 1 else "" + if direction == "IN": + self.disp( + A.color( + A.BOLD, A.FG_YELLOW, "<<<===== IN ====", A.FG_WHITE, profile_disp + ) + ) + else: + self.disp( + A.color( + A.BOLD, A.FG_CYAN, "==== OUT ====>>>", A.FG_WHITE, profile_disp + ) + ) + if whiteping: + self.disp("[WHITESPACE PING]") + else: + try: + await self.output(xml_data) + except Exception: + #  initial stream is not valid XML, + # in this case we print directly to data + #  FIXME: we should test directly lxml.etree.XMLSyntaxError + # but importing lxml directly here is not clean + # should be wrapped in a custom Exception + self.disp(xml_data) + self.disp("") + + async def start(self): + self.host.bridge.register_signal("xml_log", self.print_xml, "plugin") + + +class Theme(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, host, "theme", help=_("print colours used with your background") + ) + + def add_parser_options(self): + pass + + async def start(self): + print(f"background currently used: {A.BOLD}{self.host.background}{A.RESET}\n") + for attr in dir(C): + if not attr.startswith("A_"): + continue + color = getattr(C, attr) + if attr == "A_LEVEL_COLORS": + # This constant contains multiple colors + self.disp("LEVEL COLORS: ", end=" ") + for idx, c in enumerate(color): + last = idx == len(color) - 1 + end = "\n" if last else " " + self.disp( + c + f"LEVEL_{idx}" + A.RESET + (", " if not last else ""), end=end + ) + else: + text = attr[2:] + self.disp(A.color(color, text)) + self.host.quit() + + +class Debug(base.CommandBase): + subcommands = (bridge, Monitor, Theme) + + def __init__(self, host): + super(Debug, self).__init__( + host, "debug", use_profile=False, help=_("debugging tools") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_encryption.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_encryption.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.frontends.jp import base +from libervia.frontends.jp.constants import Const as C +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp import xmlui_manager + +__commands__ = ["Encryption"] + + +class EncryptionAlgorithms(base.CommandBase): + + def __init__(self, host): + extra_outputs = {"default": self.default_output} + super(EncryptionAlgorithms, self).__init__( + host, "algorithms", + use_output=C.OUTPUT_LIST_DICT, + extra_outputs=extra_outputs, + use_profile=False, + help=_("show available encryption algorithms")) + + def add_parser_options(self): + pass + + def default_output(self, plugins): + if not plugins: + self.disp(_("No encryption plugin registered!")) + else: + self.disp(_("Following encryption algorithms are available: {algos}").format( + algos=', '.join([p['name'] for p in plugins]))) + + async def start(self): + try: + plugins_ser = await self.host.bridge.encryption_plugins_get() + plugins = data_format.deserialise(plugins_ser, type_check=list) + except Exception as e: + self.disp(f"can't retrieve plugins: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(plugins) + self.host.quit() + + +class EncryptionGet(base.CommandBase): + + def __init__(self, host): + super(EncryptionGet, self).__init__( + host, "get", + use_output=C.OUTPUT_DICT, + help=_("get encryption session data")) + + def add_parser_options(self): + self.parser.add_argument( + "jid", + help=_("jid of the entity to check") + ) + + async def start(self): + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + try: + serialised = await self.host.bridge.message_encryption_get(jid, self.profile) + except Exception as e: + self.disp(f"can't get session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + session_data = data_format.deserialise(serialised) + if session_data is None: + self.disp( + "No encryption session found, the messages are sent in plain text.") + self.host.quit(C.EXIT_NOT_FOUND) + await self.output(session_data) + self.host.quit() + + +class EncryptionStart(base.CommandBase): + + def __init__(self, host): + super(EncryptionStart, self).__init__( + host, "start", + help=_("start encrypted session with an entity")) + + def add_parser_options(self): + self.parser.add_argument( + "--encrypt-noreplace", + action="store_true", + help=_("don't replace encryption algorithm if an other one is already used")) + algorithm = self.parser.add_mutually_exclusive_group() + algorithm.add_argument( + "-n", "--name", help=_("algorithm name (DEFAULT: choose automatically)")) + algorithm.add_argument( + "-N", "--namespace", + help=_("algorithm namespace (DEFAULT: choose automatically)")) + self.parser.add_argument( + "jid", + help=_("jid of the entity to stop encrypted session with") + ) + + async def start(self): + if self.args.name is not None: + try: + namespace = await self.host.bridge.encryption_namespace_get(self.args.name) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + elif self.args.namespace is not None: + namespace = self.args.namespace + else: + namespace = "" + + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + + try: + await self.host.bridge.message_encryption_start( + jid, namespace, not self.args.encrypt_noreplace, + self.profile) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() + + +class EncryptionStop(base.CommandBase): + + def __init__(self, host): + super(EncryptionStop, self).__init__( + host, "stop", + help=_("stop encrypted session with an entity")) + + def add_parser_options(self): + self.parser.add_argument( + "jid", + help=_("jid of the entity to stop encrypted session with") + ) + + async def start(self): + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + try: + await self.host.bridge.message_encryption_stop(jid, self.profile) + except Exception as e: + self.disp(f"can't end encrypted session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() + + +class TrustUI(base.CommandBase): + + def __init__(self, host): + super(TrustUI, self).__init__( + host, "ui", + help=_("get UI to manage trust")) + + def add_parser_options(self): + self.parser.add_argument( + "jid", + help=_("jid of the entity to stop encrypted session with") + ) + algorithm = self.parser.add_mutually_exclusive_group() + algorithm.add_argument( + "-n", "--name", help=_("algorithm name (DEFAULT: current algorithm)")) + algorithm.add_argument( + "-N", "--namespace", + help=_("algorithm namespace (DEFAULT: current algorithm)")) + + async def start(self): + if self.args.name is not None: + try: + namespace = await self.host.bridge.encryption_namespace_get(self.args.name) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + elif self.args.namespace is not None: + namespace = self.args.namespace + else: + namespace = "" + + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + + try: + xmlui_raw = await self.host.bridge.encryption_trust_ui_get( + jid, namespace, self.profile) + except Exception as e: + self.disp(f"can't get encryption session trust UI: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + xmlui = xmlui_manager.create(self.host, xmlui_raw) + await xmlui.show() + if xmlui.type != C.XMLUI_DIALOG: + await xmlui.submit_form() + self.host.quit() + +class EncryptionTrust(base.CommandBase): + subcommands = (TrustUI,) + + def __init__(self, host): + super(EncryptionTrust, self).__init__( + host, "trust", use_profile=False, help=_("trust manangement") + ) + + +class Encryption(base.CommandBase): + subcommands = (EncryptionAlgorithms, EncryptionGet, EncryptionStart, EncryptionStop, + EncryptionTrust) + + def __init__(self, host): + super(Encryption, self).__init__( + host, "encryption", use_profile=False, help=_("encryption sessions handling") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_event.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_event.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,755 @@ +#!/usr/bin/env python3 + + +# libervia-cli: Libervia CLI frontend +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import argparse +import sys + +from sqlalchemy import desc + +from libervia.backend.core.i18n import _ +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common import date_utils +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.frontends.jp import common +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp.constants import Const as C + +from . import base + +__commands__ = ["Event"] + +OUTPUT_OPT_TABLE = "table" + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_LIST_DICT, + use_pubsub=True, + pubsub_flags={C.MULTI_ITEMS, C.CACHE}, + use_verbose=True, + extra_outputs={ + "default": self.default_output, + }, + help=_("get event(s) data"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + events_data_s = await self.host.bridge.events_get( + self.args.service, + self.args.node, + self.args.items, + self.get_pubsub_extra(), + self.profile, + ) + except Exception as e: + self.disp(f"can't get events data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + events_data = data_format.deserialise(events_data_s, type_check=list) + await self.output(events_data) + self.host.quit() + + def default_output(self, events): + nb_events = len(events) + for idx, event in enumerate(events): + names = event["name"] + name = names.get("") or next(iter(names.values())) + start = event["start"] + start_human = date_utils.date_fmt( + start, "medium", tz_info=date_utils.TZ_LOCAL + ) + end = event["end"] + self.disp(A.color( + A.BOLD, start_human, A.RESET, " ", + f"({date_utils.delta2human(start, end)}) ", + C.A_HEADER, name + )) + if self.verbosity > 0: + descriptions = event.get("descriptions", []) + if descriptions: + self.disp(descriptions[0]["description"]) + if idx < (nb_events-1): + self.disp("") + + +class CategoryAction(argparse.Action): + + def __init__(self, option_strings, dest, nargs=None, metavar=None, **kwargs): + if nargs is not None or metavar is not None: + raise ValueError("nargs and metavar must not be used") + if metavar is not None: + metavar="TERM WIKIDATA_ID LANG" + if "--help" in sys.argv: + # FIXME: dirty workaround to have correct --help message + # argparse doesn't normally allow variable number of arguments beside "+" + # and "*", this workaround show METAVAR as 3 arguments were expected, while + # we can actuall use 1, 2 or 3. + nargs = 3 + metavar = ("TERM", "[WIKIDATA_ID]", "[LANG]") + else: + nargs = "+" + + super().__init__(option_strings, dest, metavar=metavar, nargs=nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + categories = getattr(namespace, self.dest) + if categories is None: + categories = [] + setattr(namespace, self.dest, categories) + + if not values: + parser.error("category values must be set") + + category = { + "term": values[0] + } + + if len(values) == 1: + pass + elif len(values) == 2: + value = values[1] + if value.startswith("Q"): + category["wikidata_id"] = value + else: + category["language"] = value + elif len(values) == 3: + __, wd, lang = values + category["wikidata_id"] = wd + category["language"] = lang + else: + parser.error("Category can't have more than 3 arguments") + + categories.append(category) + + +class EventBase: + def add_parser_options(self): + self.parser.add_argument( + "-S", "--start", type=base.date_decoder, metavar="TIME_PATTERN", + help=_("the start time of the event")) + end_group = self.parser.add_mutually_exclusive_group() + end_group.add_argument( + "-E", "--end", type=base.date_decoder, metavar="TIME_PATTERN", + help=_("the time of the end of the event")) + end_group.add_argument( + "-D", "--duration", help=_("duration of the event")) + self.parser.add_argument( + "-H", "--head-picture", help="URL to a picture to use as head-picture" + ) + self.parser.add_argument( + "-d", "--description", help="plain text description the event" + ) + self.parser.add_argument( + "-C", "--category", action=CategoryAction, dest="categories", + help="Category of the event" + ) + self.parser.add_argument( + "-l", "--location", action="append", nargs="+", metavar="[KEY] VALUE", + help="Location metadata" + ) + rsvp_group = self.parser.add_mutually_exclusive_group() + rsvp_group.add_argument( + "--rsvp", action="store_true", help=_("RSVP is requested")) + rsvp_group.add_argument( + "--rsvp_json", metavar="JSON", help=_("JSON description of RSVP form")) + for node_type in ("invitees", "comments", "blog", "schedule"): + self.parser.add_argument( + f"--{node_type}", + nargs=2, + metavar=("JID", "NODE"), + help=_("link {node_type} pubsub node").format(node_type=node_type) + ) + self.parser.add_argument( + "-a", "--attachment", action="append", dest="attachments", + help=_("attach a file") + ) + self.parser.add_argument("--website", help=_("website of the event")) + self.parser.add_argument( + "--status", choices=["confirmed", "tentative", "cancelled"], + help=_("status of the event") + ) + self.parser.add_argument( + "-T", "--language", metavar="LANG", action="append", dest="languages", + help=_("main languages spoken at the event") + ) + self.parser.add_argument( + "--wheelchair", choices=["full", "partial", "no"], + help=_("is the location accessible by wheelchair") + ) + self.parser.add_argument( + "--external", + nargs=3, + metavar=("JID", "NODE", "ITEM"), + help=_("link to an external event") + ) + + def get_event_data(self): + if self.args.duration is not None: + if self.args.start is None: + self.parser.error("--start must be send if --duration is used") + # if duration is used, we simply add it to start time to get end time + self.args.end = base.date_decoder(f"{self.args.start} + {self.args.duration}") + + event = {} + if self.args.name is not None: + event["name"] = {"": self.args.name} + + if self.args.start is not None: + event["start"] = self.args.start + + if self.args.end is not None: + event["end"] = self.args.end + + if self.args.head_picture: + event["head-picture"] = { + "sources": [{ + "url": self.args.head_picture + }] + } + if self.args.description: + event["descriptions"] = [ + { + "type": "text", + "description": self.args.description + } + ] + if self.args.categories: + event["categories"] = self.args.categories + if self.args.location is not None: + location = {} + for location_data in self.args.location: + if len(location_data) == 1: + location["description"] = location_data[0] + else: + key, *values = location_data + location[key] = " ".join(values) + event["locations"] = [location] + + if self.args.rsvp: + event["rsvp"] = [{}] + elif self.args.rsvp_json: + if isinstance(self.args.rsvp_elt, dict): + event["rsvp"] = [self.args.rsvp_json] + else: + event["rsvp"] = self.args.rsvp_json + + for node_type in ("invitees", "comments", "blog", "schedule"): + value = getattr(self.args, node_type) + if value: + service, node = value + event[node_type] = {"service": service, "node": node} + + if self.args.attachments: + attachments = event["attachments"] = [] + for attachment in self.args.attachments: + attachments.append({ + "sources": [{"url": attachment}] + }) + + extra = {} + + for arg in ("website", "status", "languages"): + value = getattr(self.args, arg) + if value is not None: + extra[arg] = value + if self.args.wheelchair is not None: + extra["accessibility"] = {"wheelchair": self.args.wheelchair} + + if extra: + event["extra"] = extra + + if self.args.external: + ext_jid, ext_node, ext_item = self.args.external + event["external"] = { + "jid": ext_jid, + "node": ext_node, + "item": ext_item + } + return event + + +class Create(EventBase, base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "create", + use_pubsub=True, + help=_("create or replace event"), + ) + + def add_parser_options(self): + super().add_parser_options() + self.parser.add_argument( + "-i", + "--id", + default="", + help=_("ID of the PubSub Item"), + ) + # name is mandatory here + self.parser.add_argument("name", help=_("name of the event")) + + async def start(self): + if self.args.start is None: + self.parser.error("--start must be set") + event_data = self.get_event_data() + # we check self.args.end after get_event_data because it may be set there id + # --duration is used + if self.args.end is None: + self.parser.error("--end or --duration must be set") + try: + await self.host.bridge.event_create( + data_format.serialise(event_data), + self.args.id, + self.args.node, + self.args.service, + self.profile, + ) + except Exception as e: + self.disp(f"can't create event: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("Event created successfuly)")) + self.host.quit() + + +class Modify(EventBase, base.CommandBase): + def __init__(self, host): + super(Modify, self).__init__( + host, + "modify", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + help=_("modify an existing event"), + ) + EventBase.__init__(self) + + def add_parser_options(self): + super().add_parser_options() + # name is optional here + self.parser.add_argument("-N", "--name", help=_("name of the event")) + + async def start(self): + event_data = self.get_event_data() + try: + await self.host.bridge.event_modify( + data_format.serialise(event_data), + self.args.item, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't update event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class InviteeGet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + use_verbose=True, + help=_("get event attendance"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-j", "--jid", action="append", dest="jids", default=[], + help=_("only retrieve RSVP from those JIDs") + ) + + async def start(self): + try: + event_data_s = await self.host.bridge.event_invitee_get( + self.args.service, + self.args.node, + self.args.item, + self.args.jids, + "", + self.profile, + ) + except Exception as e: + self.disp(f"can't get event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + event_data = data_format.deserialise(event_data_s) + await self.output(event_data) + self.host.quit() + + +class InviteeSet(base.CommandBase): + def __init__(self, host): + super(InviteeSet, self).__init__( + host, + "set", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM}, + help=_("set event attendance"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--field", + action="append", + nargs=2, + dest="fields", + metavar=("KEY", "VALUE"), + help=_("configuration field to set"), + ) + + async def start(self): + # TODO: handle RSVP with XMLUI in a similar way as for `ad-hoc run` + fields = dict(self.args.fields) if self.args.fields else {} + try: + self.host.bridge.event_invitee_set( + self.args.service, + self.args.node, + self.args.item, + data_format.serialise(fields), + self.profile, + ) + except Exception as e: + self.disp(f"can't set event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class InviteesList(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + base.CommandBase.__init__( + self, + host, + "list", + use_output=C.OUTPUT_DICT_DICT, + extra_outputs=extra_outputs, + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("get event attendance"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-m", + "--missing", + action="store_true", + help=_("show missing people (invited but no R.S.V.P. so far)"), + ) + self.parser.add_argument( + "-R", + "--no-rsvp", + action="store_true", + help=_("don't show people which gave R.S.V.P."), + ) + + def _attend_filter(self, attend, row): + if attend == "yes": + attend_color = C.A_SUCCESS + elif attend == "no": + attend_color = C.A_FAILURE + else: + attend_color = A.FG_WHITE + return A.color(attend_color, attend) + + def _guests_filter(self, guests): + return "(" + str(guests) + ")" if guests else "" + + def default_output(self, event_data): + data = [] + attendees_yes = 0 + attendees_maybe = 0 + attendees_no = 0 + attendees_missing = 0 + guests = 0 + guests_maybe = 0 + for jid_, jid_data in event_data.items(): + jid_data["jid"] = jid_ + try: + guests_int = int(jid_data["guests"]) + except (ValueError, KeyError): + pass + attend = jid_data.get("attend", "") + if attend == "yes": + attendees_yes += 1 + guests += guests_int + elif attend == "maybe": + attendees_maybe += 1 + guests_maybe += guests_int + elif attend == "no": + attendees_no += 1 + jid_data["guests"] = "" + else: + attendees_missing += 1 + jid_data["guests"] = "" + data.append(jid_data) + + show_table = OUTPUT_OPT_TABLE in self.args.output_opts + + table = common.Table.from_list_dict( + self.host, + data, + ("nick",) + (("jid",) if self.host.verbosity else ()) + ("attend", "guests"), + headers=None, + filters={ + "nick": A.color(C.A_HEADER, "{}" if show_table else "{} "), + "jid": "{}" if show_table else "{} ", + "attend": self._attend_filter, + "guests": "{}" if show_table else self._guests_filter, + }, + defaults={"nick": "", "attend": "", "guests": 1}, + ) + if show_table: + table.display() + else: + table.display_blank(show_header=False, col_sep="") + + if not self.args.no_rsvp: + self.disp("") + self.disp( + A.color( + C.A_SUBHEADER, + _("Attendees: "), + A.RESET, + str(len(data)), + _(" ("), + C.A_SUCCESS, + _("yes: "), + str(attendees_yes), + A.FG_WHITE, + _(", maybe: "), + str(attendees_maybe), + ", ", + C.A_FAILURE, + _("no: "), + str(attendees_no), + A.RESET, + ")", + ) + ) + self.disp( + A.color(C.A_SUBHEADER, _("confirmed guests: "), A.RESET, str(guests)) + ) + self.disp( + A.color( + C.A_SUBHEADER, + _("unconfirmed guests: "), + A.RESET, + str(guests_maybe), + ) + ) + self.disp( + A.color(C.A_SUBHEADER, _("total: "), A.RESET, str(guests + guests_maybe)) + ) + if attendees_missing: + self.disp("") + self.disp( + A.color( + C.A_SUBHEADER, + _("missing people (no reply): "), + A.RESET, + str(attendees_missing), + ) + ) + + async def start(self): + if self.args.no_rsvp and not self.args.missing: + self.parser.error(_("you need to use --missing if you use --no-rsvp")) + if not self.args.missing: + prefilled = {} + else: + # we get prefilled data with all people + try: + affiliations = await self.host.bridge.ps_node_affiliations_get( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get node affiliations: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + # we fill all affiliations with empty data, answered one will be filled + # below. We only consider people with "publisher" affiliation as invited, + # creators are not, and members can just observe + prefilled = { + jid_: {} + for jid_, affiliation in affiliations.items() + if affiliation in ("publisher",) + } + + try: + event_data = await self.host.bridge.event_invitees_list( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get event data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + # we fill nicknames and keep only requested people + + if self.args.no_rsvp: + for jid_ in event_data: + # if there is a jid in event_data it must be there in prefilled too + # otherwie somebody is not on the invitees list + try: + del prefilled[jid_] + except KeyError: + self.disp( + A.color( + C.A_WARNING, + f"We got a RSVP from somebody who was not in invitees " + f"list: {jid_}", + ), + error=True, + ) + else: + # we replace empty dicts for existing people with R.S.V.P. data + prefilled.update(event_data) + + # we get nicknames for everybody, make it easier for organisers + for jid_, data in prefilled.items(): + id_data = await self.host.bridge.identity_get(jid_, [], True, self.profile) + id_data = data_format.deserialise(id_data) + data["nick"] = id_data["nicknames"][0] + + await self.output(prefilled) + self.host.quit() + + +class InviteeInvite(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "invite", + use_pubsub=True, + pubsub_flags={C.NODE, C.SINGLE_ITEM}, + help=_("invite someone to the event through email"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-e", + "--email", + action="append", + default=[], + help="email(s) to send the invitation to", + ) + self.parser.add_argument( + "-N", + "--name", + default="", + help="name of the invitee", + ) + self.parser.add_argument( + "-H", + "--host-name", + default="", + help="name of the host", + ) + self.parser.add_argument( + "-l", + "--lang", + default="", + help="main language spoken by the invitee", + ) + self.parser.add_argument( + "-U", + "--url-template", + default="", + help="template to construct the URL", + ) + self.parser.add_argument( + "-S", + "--subject", + default="", + help="subject of the invitation email (default: generic subject)", + ) + self.parser.add_argument( + "-b", + "--body", + default="", + help="body of the invitation email (default: generic body)", + ) + + async def start(self): + email = self.args.email[0] if self.args.email else None + emails_extra = self.args.email[1:] + + try: + await self.host.bridge.event_invite_by_email( + self.args.service, + self.args.node, + self.args.item, + email, + emails_extra, + self.args.name, + self.args.host_name, + self.args.lang, + self.args.url_template, + self.args.subject, + self.args.body, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't create invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class Invitee(base.CommandBase): + subcommands = (InviteeGet, InviteeSet, InviteesList, InviteeInvite) + + def __init__(self, host): + super(Invitee, self).__init__( + host, "invitee", use_profile=False, help=_("manage invities") + ) + + +class Event(base.CommandBase): + subcommands = (Get, Create, Modify, Invitee) + + def __init__(self, host): + super(Event, self).__init__( + host, "event", use_profile=False, help=_("event management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_file.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_file.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1108 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from . import xmlui_manager +import sys +import os +import os.path +import tarfile +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp import common +from libervia.frontends.tools import jid +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.tools.common import utils +from urllib.parse import urlparse +from pathlib import Path +import tempfile +import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI +import json + +__commands__ = ["File"] +DEFAULT_DEST = "downloaded_file" + + +class Send(base.CommandBase): + def __init__(self, host): + super(Send, self).__init__( + host, + "send", + use_progress=True, + use_verbose=True, + help=_("send a file to a contact"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "files", type=str, nargs="+", metavar="file", help=_("a list of file") + ) + self.parser.add_argument("jid", help=_("the destination jid")) + self.parser.add_argument( + "-b", "--bz2", action="store_true", help=_("make a bzip2 tarball") + ) + self.parser.add_argument( + "-d", + "--path", + help=("path to the directory where the file must be stored"), + ) + self.parser.add_argument( + "-N", + "--namespace", + help=("namespace of the file"), + ) + self.parser.add_argument( + "-n", + "--name", + default="", + help=("name to use (DEFAULT: use source file name)"), + ) + self.parser.add_argument( + "-e", + "--encrypt", + action="store_true", + help=_("end-to-end encrypt the file transfer") + ) + + async def on_progress_started(self, metadata): + self.disp(_("File copy started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("File sent successfully"), 2) + + async def on_progress_error(self, error_msg): + if error_msg == C.PROGRESS_ERROR_DECLINED: + self.disp(_("The file has been refused by your contact")) + else: + self.disp(_("Error while sending file: {}").format(error_msg), error=True) + + async def got_id(self, data, file_): + """Called when a progress id has been received + + @param pid(unicode): progress id + @param file_(str): file path + """ + # FIXME: this show progress only for last progress_id + self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1) + try: + await self.set_progress_id(data["progress"]) + except KeyError: + # TODO: if 'xmlui' key is present, manage xmlui message display + self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True) + self.host.quit(2) + + async def start(self): + for file_ in self.args.files: + if not os.path.exists(file_): + self.disp( + _("file {file_} doesn't exist!").format(file_=repr(file_)), error=True + ) + self.host.quit(C.EXIT_BAD_ARG) + if not self.args.bz2 and os.path.isdir(file_): + self.disp( + _( + "{file_} is a dir! Please send files inside or use compression" + ).format(file_=repr(file_)) + ) + self.host.quit(C.EXIT_BAD_ARG) + + extra = {} + if self.args.path: + extra["path"] = self.args.path + if self.args.namespace: + extra["namespace"] = self.args.namespace + if self.args.encrypt: + extra["encrypted"] = True + + if self.args.bz2: + with tempfile.NamedTemporaryFile("wb", delete=False) as buf: + self.host.add_on_quit_callback(os.unlink, buf.name) + self.disp(_("bz2 is an experimental option, use with caution")) + # FIXME: check free space + self.disp(_("Starting compression, please wait...")) + sys.stdout.flush() + bz2 = tarfile.open(mode="w:bz2", fileobj=buf) + archive_name = "{}.tar.bz2".format( + os.path.basename(self.args.files[0]) or "compressed_files" + ) + for file_ in self.args.files: + self.disp(_("Adding {}").format(file_), 1) + bz2.add(file_) + bz2.close() + self.disp(_("Done !"), 1) + + try: + send_data = await self.host.bridge.file_send( + self.args.jid, + buf.name, + self.args.name or archive_name, + "", + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't send file: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.got_id(send_data, file_) + else: + for file_ in self.args.files: + path = os.path.abspath(file_) + try: + send_data = await self.host.bridge.file_send( + self.args.jid, + path, + self.args.name, + "", + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't send file {file_!r}: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.got_id(send_data, file_) + + +class Request(base.CommandBase): + def __init__(self, host): + super(Request, self).__init__( + host, + "request", + use_progress=True, + use_verbose=True, + help=_("request a file from a contact"), + ) + + @property + def filename(self): + return self.args.name or self.args.hash or "output" + + def add_parser_options(self): + self.parser.add_argument("jid", help=_("the destination jid")) + self.parser.add_argument( + "-D", + "--dest", + help=_( + "destination path where the file will be saved (default: " + "[current_dir]/[name|hash])" + ), + ) + self.parser.add_argument( + "-n", + "--name", + default="", + help=_("name of the file"), + ) + self.parser.add_argument( + "-H", + "--hash", + default="", + help=_("hash of the file"), + ) + self.parser.add_argument( + "-a", + "--hash-algo", + default="sha-256", + help=_("hash algorithm use for --hash (default: sha-256)"), + ) + self.parser.add_argument( + "-d", + "--path", + help=("path to the directory containing the file"), + ) + self.parser.add_argument( + "-N", + "--namespace", + help=("namespace of the file"), + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("overwrite existing file without confirmation"), + ) + + async def on_progress_started(self, metadata): + self.disp(_("File copy started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("File received successfully"), 2) + + async def on_progress_error(self, error_msg): + if error_msg == C.PROGRESS_ERROR_DECLINED: + self.disp(_("The file request has been refused")) + else: + self.disp(_("Error while requesting file: {}").format(error_msg), error=True) + + async def start(self): + if not self.args.name and not self.args.hash: + self.parser.error(_("at least one of --name or --hash must be provided")) + if self.args.dest: + path = os.path.abspath(os.path.expanduser(self.args.dest)) + if os.path.isdir(path): + path = os.path.join(path, self.filename) + else: + path = os.path.abspath(self.filename) + + if os.path.exists(path) and not self.args.force: + message = _("File {path} already exists! Do you want to overwrite?").format( + path=path + ) + await self.host.confirm_or_quit(message, _("file request cancelled")) + + self.full_dest_jid = await self.host.get_full_jid(self.args.jid) + extra = {} + if self.args.path: + extra["path"] = self.args.path + if self.args.namespace: + extra["namespace"] = self.args.namespace + try: + progress_id = await self.host.bridge.file_jingle_request( + self.full_dest_jid, + path, + self.args.name, + self.args.hash, + self.args.hash_algo if self.args.hash else "", + extra, + self.profile, + ) + except Exception as e: + self.disp(msg=_("can't request file: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.set_progress_id(progress_id) + + +class Receive(base.CommandAnswering): + def __init__(self, host): + super(Receive, self).__init__( + host, + "receive", + use_progress=True, + use_verbose=True, + help=_("wait for a file to be sent by a contact"), + ) + self._overwrite_refused = False # True when one overwrite as already been refused + self.action_callbacks = { + C.META_TYPE_FILE: self.on_file_action, + C.META_TYPE_OVERWRITE: self.on_overwrite_action, + C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action, + } + + def add_parser_options(self): + self.parser.add_argument( + "jids", + nargs="*", + help=_("jids accepted (accept everything if none is specified)"), + ) + self.parser.add_argument( + "-m", + "--multiple", + action="store_true", + help=_("accept multiple files (you'll have to stop manually)"), + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_( + "force overwritting of existing files (/!\\ name is choosed by sender)" + ), + ) + self.parser.add_argument( + "--path", + default=".", + metavar="DIR", + help=_("destination path (default: working directory)"), + ) + + async def on_progress_started(self, metadata): + self.disp(_("File copy started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("File received successfully"), 2) + if metadata.get("hash_verified", False): + try: + self.disp( + _("hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1 + ) + except KeyError: + self.disp(_("hash is checked but hash value is missing", 1), error=True) + else: + self.disp(_("hash can't be verified"), 1) + + async def on_progress_error(self, e): + self.disp(_("Error while receiving file: {e}").format(e=e), error=True) + + def get_xmlui_id(self, action_data): + # FIXME: we temporarily use ElementTree, but a real XMLUI managing module + # should be available in the futur + # TODO: XMLUI module + try: + xml_ui = action_data["xmlui"] + except KeyError: + self.disp(_("Action has no XMLUI"), 1) + else: + ui = ET.fromstring(xml_ui.encode("utf-8")) + xmlui_id = ui.get("submit") + if not xmlui_id: + self.disp(_("Invalid XMLUI received"), error=True) + return xmlui_id + + async def on_file_action(self, action_data, action_id, security_limit, profile): + xmlui_id = self.get_xmlui_id(action_data) + if xmlui_id is None: + return self.host.quit_from_signal(1) + try: + from_jid = jid.JID(action_data["from_jid"]) + except KeyError: + self.disp(_("Ignoring action without from_jid data"), 1) + return + try: + progress_id = action_data["progress_id"] + except KeyError: + self.disp(_("ignoring action without progress id"), 1) + return + + if not self.bare_jids or from_jid.bare in self.bare_jids: + if self._overwrite_refused: + self.disp(_("File refused because overwrite is needed"), error=True) + await self.host.bridge.action_launch( + xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}), + profile_key=profile + ) + return self.host.quit_from_signal(2) + await self.set_progress_id(progress_id) + xmlui_data = {"path": self.path} + await self.host.bridge.action_launch( + xmlui_id, data_format.serialise(xmlui_data), profile_key=profile + ) + + async def on_overwrite_action(self, action_data, action_id, security_limit, profile): + xmlui_id = self.get_xmlui_id(action_data) + if xmlui_id is None: + return self.host.quit_from_signal(1) + try: + progress_id = action_data["progress_id"] + except KeyError: + self.disp(_("ignoring action without progress id"), 1) + return + self.disp(_("Overwriting needed"), 1) + + if progress_id == self.progress_id: + if self.args.force: + self.disp(_("Overwrite accepted"), 2) + else: + self.disp(_("Refused to overwrite"), 2) + self._overwrite_refused = True + + xmlui_data = {"answer": C.bool_const(self.args.force)} + await self.host.bridge.action_launch( + xmlui_id, data_format.serialise(xmlui_data), profile_key=profile + ) + + async def on_not_in_roster_action(self, action_data, action_id, security_limit, profile): + xmlui_id = self.get_xmlui_id(action_data) + if xmlui_id is None: + return self.host.quit_from_signal(1) + try: + from_jid = jid.JID(action_data["from_jid"]) + except ValueError: + self.disp( + _('invalid "from_jid" value received, ignoring: {value}').format( + value=from_jid + ), + error=True, + ) + return + except KeyError: + self.disp(_('ignoring action without "from_jid" value'), error=True) + return + self.disp(_("Confirmation needed for request from an entity not in roster"), 1) + + if from_jid.bare in self.bare_jids: + # if the sender is expected, we can confirm the session + confirmed = True + self.disp(_("Sender confirmed because she or he is explicitly expected"), 1) + else: + xmlui = xmlui_manager.create(self.host, action_data["xmlui"]) + confirmed = await self.host.confirm(xmlui.dlg.message) + + xmlui_data = {"answer": C.bool_const(confirmed)} + await self.host.bridge.action_launch( + xmlui_id, data_format.serialise(xmlui_data), profile_key=profile + ) + if not confirmed and not self.args.multiple: + self.disp(_("Session refused for {from_jid}").format(from_jid=from_jid)) + self.host.quit_from_signal(0) + + async def start(self): + self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] + self.path = os.path.abspath(self.args.path) + if not os.path.isdir(self.path): + self.disp(_("Given path is not a directory !", error=True)) + self.host.quit(C.EXIT_BAD_ARG) + if self.args.multiple: + self.host.quit_on_progress_end = False + self.disp(_("waiting for incoming file request"), 2) + await self.start_answering() + + +class Get(base.CommandBase): + def __init__(self, host): + super(Get, self).__init__( + host, + "get", + use_progress=True, + use_verbose=True, + help=_("download a file from URI"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-o", + "--dest-file", + type=str, + default="", + help=_("destination file (DEFAULT: filename from URL)"), + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("overwrite existing file without confirmation"), + ) + self.parser.add_argument( + "attachment", type=str, + help=_("URI of the file to retrieve or JSON of the whole attachment") + ) + + async def on_progress_started(self, metadata): + self.disp(_("File download started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("File downloaded successfully"), 2) + + async def on_progress_error(self, error_msg): + self.disp(_("Error while downloading file: {}").format(error_msg), error=True) + + async def got_id(self, data): + """Called when a progress id has been received""" + try: + await self.set_progress_id(data["progress"]) + except KeyError: + if "xmlui" in data: + ui = xmlui_manager.create(self.host, data["xmlui"]) + await ui.show() + else: + self.disp(_("Can't download file"), error=True) + self.host.quit(C.EXIT_ERROR) + + async def start(self): + try: + attachment = json.loads(self.args.attachment) + except json.JSONDecodeError: + attachment = {"uri": self.args.attachment} + dest_file = self.args.dest_file + if not dest_file: + try: + dest_file = attachment["name"].replace("/", "-").strip() + except KeyError: + try: + dest_file = Path(urlparse(attachment["uri"]).path).name.strip() + except KeyError: + pass + if not dest_file: + dest_file = "downloaded_file" + + dest_file = Path(dest_file).expanduser().resolve() + if dest_file.exists() and not self.args.force: + message = _("File {path} already exists! Do you want to overwrite?").format( + path=dest_file + ) + await self.host.confirm_or_quit(message, _("file download cancelled")) + + options = {} + + try: + download_data_s = await self.host.bridge.file_download( + data_format.serialise(attachment), + str(dest_file), + data_format.serialise(options), + self.profile, + ) + download_data = data_format.deserialise(download_data_s) + except Exception as e: + self.disp(f"error while trying to download a file: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.got_id(download_data) + + +class Upload(base.CommandBase): + def __init__(self, host): + super(Upload, self).__init__( + host, "upload", use_progress=True, use_verbose=True, help=_("upload a file") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-e", + "--encrypt", + action="store_true", + help=_("encrypt file using AES-GCM"), + ) + self.parser.add_argument("file", type=str, help=_("file to upload")) + self.parser.add_argument( + "jid", + nargs="?", + help=_("jid of upload component (nothing to autodetect)"), + ) + self.parser.add_argument( + "--ignore-tls-errors", + action="store_true", + help=_(r"ignore invalide TLS certificate (/!\ Dangerous /!\)"), + ) + + async def on_progress_started(self, metadata): + self.disp(_("File upload started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("File uploaded successfully"), 2) + try: + url = metadata["url"] + except KeyError: + self.disp("download URL not found in metadata") + else: + self.disp(_("URL to retrieve the file:"), 1) + # XXX: url is displayed alone on a line to make parsing easier + self.disp(url) + + async def on_progress_error(self, error_msg): + self.disp(_("Error while uploading file: {}").format(error_msg), error=True) + + async def got_id(self, data, file_): + """Called when a progress id has been received + + @param pid(unicode): progress id + @param file_(str): file path + """ + try: + await self.set_progress_id(data["progress"]) + except KeyError: + if "xmlui" in data: + ui = xmlui_manager.create(self.host, data["xmlui"]) + await ui.show() + else: + self.disp(_("Can't upload file"), error=True) + self.host.quit(C.EXIT_ERROR) + + async def start(self): + file_ = self.args.file + if not os.path.exists(file_): + self.disp( + _("file {file_} doesn't exist !").format(file_=repr(file_)), error=True + ) + self.host.quit(C.EXIT_BAD_ARG) + if os.path.isdir(file_): + self.disp(_("{file_} is a dir! Can't upload a dir").format(file_=repr(file_))) + self.host.quit(C.EXIT_BAD_ARG) + + if self.args.jid is None: + self.full_dest_jid = "" + else: + self.full_dest_jid = await self.host.get_full_jid(self.args.jid) + + options = {} + if self.args.ignore_tls_errors: + options["ignore_tls_errors"] = True + if self.args.encrypt: + options["encryption"] = C.ENC_AES_GCM + + path = os.path.abspath(file_) + try: + upload_data = await self.host.bridge.file_upload( + path, + "", + self.full_dest_jid, + data_format.serialise(options), + self.profile, + ) + except Exception as e: + self.disp(f"error while trying to upload a file: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.got_id(upload_data, file_) + + +class ShareAffiliationsSet(base.CommandBase): + def __init__(self, host): + super(ShareAffiliationsSet, self).__init__( + host, + "set", + use_output=C.OUTPUT_DICT, + help=_("set affiliations for a shared file/directory"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-N", + "--namespace", + default="", + help=_("namespace of the repository"), + ) + self.parser.add_argument( + "-P", + "--path", + default="", + help=_("path to the repository"), + ) + self.parser.add_argument( + "-a", + "--affiliation", + dest="affiliations", + metavar=("JID", "AFFILIATION"), + required=True, + action="append", + nargs=2, + help=_("entity/affiliation couple(s)"), + ) + self.parser.add_argument( + "jid", + help=_("jid of file sharing entity"), + ) + + async def start(self): + affiliations = dict(self.args.affiliations) + try: + affiliations = await self.host.bridge.fis_affiliations_set( + self.args.jid, + self.args.namespace, + self.args.path, + affiliations, + self.profile, + ) + except Exception as e: + self.disp(f"can't set affiliations: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class ShareAffiliationsGet(base.CommandBase): + def __init__(self, host): + super(ShareAffiliationsGet, self).__init__( + host, + "get", + use_output=C.OUTPUT_DICT, + help=_("retrieve affiliations of a shared file/directory"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-N", + "--namespace", + default="", + help=_("namespace of the repository"), + ) + self.parser.add_argument( + "-P", + "--path", + default="", + help=_("path to the repository"), + ) + self.parser.add_argument( + "jid", + help=_("jid of sharing entity"), + ) + + async def start(self): + try: + affiliations = await self.host.bridge.fis_affiliations_get( + self.args.jid, + self.args.namespace, + self.args.path, + self.profile, + ) + except Exception as e: + self.disp(f"can't get affiliations: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(affiliations) + self.host.quit() + + +class ShareAffiliations(base.CommandBase): + subcommands = (ShareAffiliationsGet, ShareAffiliationsSet) + + def __init__(self, host): + super(ShareAffiliations, self).__init__( + host, "affiliations", use_profile=False, help=_("affiliations management") + ) + + +class ShareConfigurationSet(base.CommandBase): + def __init__(self, host): + super(ShareConfigurationSet, self).__init__( + host, + "set", + use_output=C.OUTPUT_DICT, + help=_("set configuration for a shared file/directory"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-N", + "--namespace", + default="", + help=_("namespace of the repository"), + ) + self.parser.add_argument( + "-P", + "--path", + default="", + help=_("path to the repository"), + ) + self.parser.add_argument( + "-f", + "--field", + action="append", + nargs=2, + dest="fields", + required=True, + metavar=("KEY", "VALUE"), + help=_("configuration field to set (required)"), + ) + self.parser.add_argument( + "jid", + help=_("jid of file sharing entity"), + ) + + async def start(self): + configuration = dict(self.args.fields) + try: + configuration = await self.host.bridge.fis_configuration_set( + self.args.jid, + self.args.namespace, + self.args.path, + configuration, + self.profile, + ) + except Exception as e: + self.disp(f"can't set configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class ShareConfigurationGet(base.CommandBase): + def __init__(self, host): + super(ShareConfigurationGet, self).__init__( + host, + "get", + use_output=C.OUTPUT_DICT, + help=_("retrieve configuration of a shared file/directory"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-N", + "--namespace", + default="", + help=_("namespace of the repository"), + ) + self.parser.add_argument( + "-P", + "--path", + default="", + help=_("path to the repository"), + ) + self.parser.add_argument( + "jid", + help=_("jid of sharing entity"), + ) + + async def start(self): + try: + configuration = await self.host.bridge.fis_configuration_get( + self.args.jid, + self.args.namespace, + self.args.path, + self.profile, + ) + except Exception as e: + self.disp(f"can't get configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(configuration) + self.host.quit() + + +class ShareConfiguration(base.CommandBase): + subcommands = (ShareConfigurationGet, ShareConfigurationSet) + + def __init__(self, host): + super(ShareConfiguration, self).__init__( + host, + "configuration", + use_profile=False, + help=_("file sharing node configuration"), + ) + + +class ShareList(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + super(ShareList, self).__init__( + host, + "list", + use_output=C.OUTPUT_LIST_DICT, + extra_outputs=extra_outputs, + help=_("retrieve files shared by an entity"), + use_verbose=True, + ) + + def add_parser_options(self): + self.parser.add_argument( + "-d", + "--path", + default="", + help=_("path to the directory containing the files"), + ) + self.parser.add_argument( + "jid", + nargs="?", + default="", + help=_("jid of sharing entity (nothing to check our own jid)"), + ) + + def _name_filter(self, name, row): + if row.type == C.FILE_TYPE_DIRECTORY: + return A.color(C.A_DIRECTORY, name) + elif row.type == C.FILE_TYPE_FILE: + return A.color(C.A_FILE, name) + else: + self.disp(_("unknown file type: {type}").format(type=row.type), error=True) + return name + + def _size_filter(self, size, row): + if not size: + return "" + return A.color(A.BOLD, utils.get_human_size(size)) + + def default_output(self, files_data): + """display files a way similar to ls""" + files_data.sort(key=lambda d: d["name"].lower()) + show_header = False + if self.verbosity == 0: + keys = headers = ("name", "type") + elif self.verbosity == 1: + keys = headers = ("name", "type", "size") + elif self.verbosity > 1: + show_header = True + keys = ("name", "type", "size", "file_hash") + headers = ("name", "type", "size", "hash") + table = common.Table.from_list_dict( + self.host, + files_data, + keys=keys, + headers=headers, + filters={"name": self._name_filter, "size": self._size_filter}, + defaults={"size": "", "file_hash": ""}, + ) + table.display_blank(show_header=show_header, hide_cols=["type"]) + + async def start(self): + try: + files_data = await self.host.bridge.fis_list( + self.args.jid, + self.args.path, + {}, + self.profile, + ) + except Exception as e: + self.disp(f"can't retrieve shared files: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + await self.output(files_data) + self.host.quit() + + +class SharePath(base.CommandBase): + def __init__(self, host): + super(SharePath, self).__init__( + host, "path", help=_("share a file or directory"), use_verbose=True + ) + + def add_parser_options(self): + self.parser.add_argument( + "-n", + "--name", + default="", + help=_("virtual name to use (default: use directory/file name)"), + ) + perm_group = self.parser.add_mutually_exclusive_group() + perm_group.add_argument( + "-j", + "--jid", + metavar="JID", + action="append", + dest="jids", + default=[], + help=_("jid of contacts allowed to retrieve the files"), + ) + perm_group.add_argument( + "--public", + action="store_true", + help=_( + r"share publicly the file(s) (/!\ *everybody* will be able to access " + r"them)" + ), + ) + self.parser.add_argument( + "path", + help=_("path to a file or directory to share"), + ) + + async def start(self): + self.path = os.path.abspath(self.args.path) + if self.args.public: + access = {"read": {"type": "public"}} + else: + jids = self.args.jids + if jids: + access = {"read": {"type": "whitelist", "jids": jids}} + else: + access = {} + try: + name = await self.host.bridge.fis_share_path( + self.args.name, + self.path, + json.dumps(access, ensure_ascii=False), + self.profile, + ) + except Exception as e: + self.disp(f"can't share path: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp( + _('{path} shared under the name "{name}"').format( + path=self.path, name=name + ) + ) + self.host.quit() + + +class ShareInvite(base.CommandBase): + def __init__(self, host): + super(ShareInvite, self).__init__( + host, "invite", help=_("send invitation for a shared repository") + ) + + def add_parser_options(self): + self.parser.add_argument( + "-n", + "--name", + default="", + help=_("name of the repository"), + ) + self.parser.add_argument( + "-N", + "--namespace", + default="", + help=_("namespace of the repository"), + ) + self.parser.add_argument( + "-P", + "--path", + help=_("path to the repository"), + ) + self.parser.add_argument( + "-t", + "--type", + choices=["files", "photos"], + default="files", + help=_("type of the repository"), + ) + self.parser.add_argument( + "-T", + "--thumbnail", + help=_("https URL of a image to use as thumbnail"), + ) + self.parser.add_argument( + "service", + help=_("jid of the file sharing service hosting the repository"), + ) + self.parser.add_argument( + "jid", + help=_("jid of the person to invite"), + ) + + async def start(self): + self.path = os.path.normpath(self.args.path) if self.args.path else "" + extra = {} + if self.args.thumbnail is not None: + if not self.args.thumbnail.startswith("http"): + self.parser.error(_("only http(s) links are allowed with --thumbnail")) + else: + extra["thumb_url"] = self.args.thumbnail + try: + await self.host.bridge.fis_invite( + self.args.jid, + self.args.service, + self.args.type, + self.args.namespace, + self.path, + self.args.name, + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't send invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("invitation sent to {jid}").format(jid=self.args.jid)) + self.host.quit() + + +class Share(base.CommandBase): + subcommands = ( + ShareList, + SharePath, + ShareInvite, + ShareAffiliations, + ShareConfiguration, + ) + + def __init__(self, host): + super(Share, self).__init__( + host, "share", use_profile=False, help=_("files sharing management") + ) + + +class File(base.CommandBase): + subcommands = (Send, Request, Receive, Get, Upload, Share) + + def __init__(self, host): + super(File, self).__init__( + host, "file", use_profile=False, help=_("files sending/receiving/management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_forums.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_forums.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp import common +from libervia.backend.tools.common.ansi import ANSI as A +import codecs +import json + +__commands__ = ["Forums"] + +FORUMS_TMP_DIR = "forums" + + +class Edit(base.CommandBase, common.BaseEdit): + use_items = False + + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "edit", + use_pubsub=True, + use_draft=True, + use_verbose=True, + help=_("edit forums"), + ) + common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR) + + def add_parser_options(self): + self.parser.add_argument( + "-k", + "--key", + default="", + help=_("forum key (DEFAULT: default forums)"), + ) + + def get_tmp_suff(self): + """return suffix used for content file""" + return "json" + + async def publish(self, forums_raw): + try: + await self.host.bridge.forums_set( + forums_raw, + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + self.disp(f"can't set forums: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("forums have been edited"), 1) + self.host.quit() + + async def start(self): + try: + forums_json = await self.host.bridge.forums_get( + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + if e.classname == "NotFound": + forums_json = "" + else: + self.disp(f"can't get node configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + content_file_obj, content_file_path = self.get_tmp_file() + forums_json = forums_json.strip() + if forums_json: + # we loads and dumps to have pretty printed json + forums = json.loads(forums_json) + # cf. https://stackoverflow.com/a/18337754 + f = codecs.getwriter("utf-8")(content_file_obj) + json.dump(forums, f, ensure_ascii=False, indent=4) + content_file_obj.seek(0) + await self.run_editor("forums_editor_args", content_file_path, content_file_obj) + + +class Get(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_COMPLEX, + extra_outputs=extra_outputs, + use_pubsub=True, + use_verbose=True, + help=_("get forums structure"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-k", + "--key", + default="", + help=_("forum key (DEFAULT: default forums)"), + ) + + def default_output(self, forums, level=0): + for forum in forums: + keys = list(forum.keys()) + keys.sort() + try: + keys.remove("title") + except ValueError: + pass + else: + keys.insert(0, "title") + try: + keys.remove("sub-forums") + except ValueError: + pass + else: + keys.append("sub-forums") + + for key in keys: + value = forum[key] + if key == "sub-forums": + self.default_output(value, level + 1) + else: + if self.host.verbosity < 1 and key != "title": + continue + head_color = C.A_LEVEL_COLORS[level % len(C.A_LEVEL_COLORS)] + self.disp( + A.color(level * 4 * " ", head_color, key, A.RESET, ": ", value) + ) + + async def start(self): + try: + forums_raw = await self.host.bridge.forums_get( + self.args.service, + self.args.node, + self.args.key, + self.profile, + ) + except Exception as e: + self.disp(f"can't get forums: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if not forums_raw: + self.disp(_("no schema found"), 1) + self.host.quit(1) + forums = json.loads(forums_raw) + await self.output(forums) + self.host.quit() + + +class Forums(base.CommandBase): + subcommands = (Get, Edit) + + def __init__(self, host): + super(Forums, self).__init__( + host, "forums", use_profile=False, help=_("Forums structure edition") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_identity.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_identity.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.backend.tools.common import data_format + +__commands__ = ["Identity"] + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_DICT, + use_verbose=True, + help=_("get identity data"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "--no-cache", action="store_true", help=_("do no use cached values") + ) + self.parser.add_argument( + "jid", help=_("entity to check") + ) + + async def start(self): + jid_ = (await self.host.check_jids([self.args.jid]))[0] + try: + data = await self.host.bridge.identity_get( + jid_, + [], + not self.args.no_cache, + self.profile + ) + except Exception as e: + self.disp(f"can't get identity data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + data = data_format.deserialise(data) + await self.output(data) + self.host.quit() + + +class Set(base.CommandBase): + def __init__(self, host): + super(Set, self).__init__(host, "set", help=_("update identity data")) + + def add_parser_options(self): + self.parser.add_argument( + "-n", + "--nickname", + action="append", + metavar="NICKNAME", + dest="nicknames", + help=_("nicknames of the entity"), + ) + self.parser.add_argument( + "-d", + "--description", + help=_("description of the entity"), + ) + + async def start(self): + id_data = {} + for field in ("nicknames", "description"): + value = getattr(self.args, field) + if value is not None: + id_data[field] = value + if not id_data: + self.parser.error("At least one metadata must be set") + try: + self.host.bridge.identity_set( + data_format.serialise(id_data), + self.profile, + ) + except Exception as e: + self.disp(f"can't set identity data: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class Identity(base.CommandBase): + subcommands = (Get, Set) + + def __init__(self, host): + super(Identity, self).__init__( + host, "identity", use_profile=False, help=_("identity management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_info.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_info.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from pprint import pformat + +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format, date_utils +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.frontends.jp import common +from libervia.frontends.jp.constants import Const as C + +from . import base + +__commands__ = ["Info"] + + +class Disco(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + super(Disco, self).__init__( + host, + "disco", + use_output="complex", + extra_outputs=extra_outputs, + help=_("service discovery"), + ) + + def add_parser_options(self): + self.parser.add_argument("jid", help=_("entity to discover")) + self.parser.add_argument( + "-t", + "--type", + type=str, + choices=("infos", "items", "both", "external", "all"), + default="all", + help=_("type of data to discover"), + ) + self.parser.add_argument("-n", "--node", default="", help=_("node to use")) + self.parser.add_argument( + "-C", + "--no-cache", + dest="use_cache", + action="store_false", + help=_("ignore cache"), + ) + + def default_output(self, data): + features = data.get("features", []) + identities = data.get("identities", []) + extensions = data.get("extensions", {}) + items = data.get("items", []) + external = data.get("external", []) + + identities_table = common.Table( + self.host, + identities, + headers=(_("category"), _("type"), _("name")), + use_buffer=True, + ) + + extensions_tpl = [] + extensions_types = list(extensions.keys()) + extensions_types.sort() + for type_ in extensions_types: + fields = [] + for field in extensions[type_]: + field_lines = [] + data, values = field + data_keys = list(data.keys()) + data_keys.sort() + for key in data_keys: + field_lines.append( + A.color("\t", C.A_SUBHEADER, key, A.RESET, ": ", data[key]) + ) + if len(values) == 1: + field_lines.append( + A.color( + "\t", + C.A_SUBHEADER, + "value", + A.RESET, + ": ", + values[0] or (A.BOLD + "UNSET"), + ) + ) + elif len(values) > 1: + field_lines.append( + A.color("\t", C.A_SUBHEADER, "values", A.RESET, ": ") + ) + + for value in values: + field_lines.append(A.color("\t - ", A.BOLD, value)) + fields.append("\n".join(field_lines)) + extensions_tpl.append( + "{type_}\n{fields}".format(type_=type_, fields="\n\n".join(fields)) + ) + + items_table = common.Table( + self.host, items, headers=(_("entity"), _("node"), _("name")), use_buffer=True + ) + + template = [] + fmt_kwargs = {} + if features: + template.append(A.color(C.A_HEADER, _("Features")) + "\n\n{features}") + if identities: + template.append(A.color(C.A_HEADER, _("Identities")) + "\n\n{identities}") + if extensions: + template.append(A.color(C.A_HEADER, _("Extensions")) + "\n\n{extensions}") + if items: + template.append(A.color(C.A_HEADER, _("Items")) + "\n\n{items}") + if external: + fmt_lines = [] + for e in external: + data = {k: e[k] for k in sorted(e)} + host = data.pop("host") + type_ = data.pop("type") + fmt_lines.append(A.color( + "\t", + C.A_SUBHEADER, + host, + " ", + A.RESET, + "[", + C.A_LEVEL_COLORS[1], + type_, + A.RESET, + "]", + )) + extended = data.pop("extended", None) + for key, value in data.items(): + fmt_lines.append(A.color( + "\t\t", + C.A_LEVEL_COLORS[2], + f"{key}: ", + C.A_LEVEL_COLORS[3], + str(value) + )) + if extended: + fmt_lines.append(A.color( + "\t\t", + C.A_HEADER, + "extended", + )) + nb_extended = len(extended) + for idx, form_data in enumerate(extended): + namespace = form_data.get("namespace") + if namespace: + fmt_lines.append(A.color( + "\t\t", + C.A_LEVEL_COLORS[2], + "namespace: ", + C.A_LEVEL_COLORS[3], + A.BOLD, + namespace + )) + for field_data in form_data["fields"]: + name = field_data.get("name") + if not name: + continue + field_type = field_data.get("type") + if "multi" in field_type: + value = ", ".join(field_data.get("values") or []) + else: + value = field_data.get("value") + if value is None: + continue + if field_type == "boolean": + value = C.bool(value) + fmt_lines.append(A.color( + "\t\t", + C.A_LEVEL_COLORS[2], + f"{name}: ", + C.A_LEVEL_COLORS[3], + A.BOLD, + str(value) + )) + if nb_extended>1 and idx < nb_extended-1: + fmt_lines.append("\n") + + fmt_lines.append("\n") + + template.append( + A.color(C.A_HEADER, _("External")) + "\n\n{external_formatted}" + ) + fmt_kwargs["external_formatted"] = "\n".join(fmt_lines) + + print( + "\n\n".join(template).format( + features="\n".join(features), + identities=identities_table.display().string, + extensions="\n".join(extensions_tpl), + items=items_table.display().string, + **fmt_kwargs, + ) + ) + + async def start(self): + infos_requested = self.args.type in ("infos", "both", "all") + items_requested = self.args.type in ("items", "both", "all") + exter_requested = self.args.type in ("external", "all") + if self.args.node: + if self.args.type == "external": + self.parser.error( + '--node can\'t be used with discovery of external services ' + '(--type="external")' + ) + else: + exter_requested = False + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + data = {} + + # infos + if infos_requested: + try: + infos = await self.host.bridge.disco_infos( + jid, + node=self.args.node, + use_cache=self.args.use_cache, + profile_key=self.host.profile, + ) + except Exception as e: + self.disp(_("error while doing discovery: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + else: + features, identities, extensions = infos + features.sort() + identities.sort(key=lambda identity: identity[2]) + data.update( + {"features": features, "identities": identities, "extensions": extensions} + ) + + # items + if items_requested: + try: + items = await self.host.bridge.disco_items( + jid, + node=self.args.node, + use_cache=self.args.use_cache, + profile_key=self.host.profile, + ) + except Exception as e: + self.disp(_("error while doing discovery: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + items.sort(key=lambda item: item[2]) + data["items"] = items + + # external + if exter_requested: + try: + ext_services_s = await self.host.bridge.external_disco_get( + jid, + self.host.profile, + ) + except Exception as e: + self.disp( + _("error while doing external service discovery: {e}").format(e=e), + error=True + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + data["external"] = data_format.deserialise( + ext_services_s, type_check=list + ) + + # output + await self.output(data) + self.host.quit() + + +class Version(base.CommandBase): + def __init__(self, host): + super(Version, self).__init__(host, "version", help=_("software version")) + + def add_parser_options(self): + self.parser.add_argument("jid", type=str, help=_("Entity to request")) + + async def start(self): + jids = await self.host.check_jids([self.args.jid]) + jid = jids[0] + try: + data = await self.host.bridge.software_version_get(jid, self.host.profile) + except Exception as e: + self.disp(_("error while trying to get version: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + infos = [] + name, version, os = data + if name: + infos.append(_("Software name: {name}").format(name=name)) + if version: + infos.append(_("Software version: {version}").format(version=version)) + if os: + infos.append(_("Operating System: {os}").format(os=os)) + + print("\n".join(infos)) + self.host.quit() + + +class Session(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + super(Session, self).__init__( + host, + "session", + use_output="dict", + extra_outputs=extra_outputs, + help=_("running session"), + ) + + def add_parser_options(self): + pass + + async def default_output(self, data): + started = data["started"] + data["started"] = "{short} (UTC, {relative})".format( + short=date_utils.date_fmt(started), + relative=date_utils.date_fmt(started, "relative"), + ) + await self.host.output(C.OUTPUT_DICT, "simple", {}, data) + + async def start(self): + try: + data = await self.host.bridge.session_infos_get(self.host.profile) + except Exception as e: + self.disp(_("Error getting session infos: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(data) + self.host.quit() + + +class Devices(base.CommandBase): + def __init__(self, host): + super(Devices, self).__init__( + host, "devices", use_output=C.OUTPUT_LIST_DICT, help=_("devices of an entity") + ) + + def add_parser_options(self): + self.parser.add_argument( + "jid", type=str, nargs="?", default="", help=_("Entity to request") + ) + + async def start(self): + try: + data = await self.host.bridge.devices_infos_get( + self.args.jid, self.host.profile + ) + except Exception as e: + self.disp(_("Error getting devices infos: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + data = data_format.deserialise(data, type_check=list) + await self.output(data) + self.host.quit() + + +class Info(base.CommandBase): + subcommands = (Disco, Version, Session, Devices) + + def __init__(self, host): + super(Info, self).__init__( + host, + "info", + use_profile=False, + help=_("Get various pieces of information on entities"), + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_input.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_input.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import subprocess +import argparse +import sys +import shlex +import asyncio +from . import base +from libervia.backend.core.i18n import _ +from libervia.backend.core import exceptions +from libervia.frontends.jp.constants import Const as C +from libervia.backend.tools.common.ansi import ANSI as A + +__commands__ = ["Input"] +OPT_STDIN = "stdin" +OPT_SHORT = "short" +OPT_LONG = "long" +OPT_POS = "positional" +OPT_IGNORE = "ignore" +OPT_TYPES = (OPT_STDIN, OPT_SHORT, OPT_LONG, OPT_POS, OPT_IGNORE) +OPT_EMPTY_SKIP = "skip" +OPT_EMPTY_IGNORE = "ignore" +OPT_EMPTY_CHOICES = (OPT_EMPTY_SKIP, OPT_EMPTY_IGNORE) + + +class InputCommon(base.CommandBase): + def __init__(self, host, name, help): + base.CommandBase.__init__( + self, host, name, use_verbose=True, use_profile=False, help=help + ) + self.idx = 0 + self.reset() + + def reset(self): + self.args_idx = 0 + self._stdin = [] + self._opts = [] + self._pos = [] + self._values_ori = [] + + def add_parser_options(self): + self.parser.add_argument( + "--encoding", default="utf-8", help=_("encoding of the input data") + ) + self.parser.add_argument( + "-i", + "--stdin", + action="append_const", + const=(OPT_STDIN, None), + dest="arguments", + help=_("standard input"), + ) + self.parser.add_argument( + "-s", + "--short", + type=self.opt(OPT_SHORT), + action="append", + dest="arguments", + help=_("short option"), + ) + self.parser.add_argument( + "-l", + "--long", + type=self.opt(OPT_LONG), + action="append", + dest="arguments", + help=_("long option"), + ) + self.parser.add_argument( + "-p", + "--positional", + type=self.opt(OPT_POS), + action="append", + dest="arguments", + help=_("positional argument"), + ) + self.parser.add_argument( + "-x", + "--ignore", + action="append_const", + const=(OPT_IGNORE, None), + dest="arguments", + help=_("ignore value"), + ) + self.parser.add_argument( + "-D", + "--debug", + action="store_true", + help=_("don't actually run commands but echo what would be launched"), + ) + self.parser.add_argument( + "--log", type=argparse.FileType("w"), help=_("log stdout to FILE") + ) + self.parser.add_argument( + "--log-err", type=argparse.FileType("w"), help=_("log stderr to FILE") + ) + self.parser.add_argument("command", nargs=argparse.REMAINDER) + + def opt(self, type_): + return lambda s: (type_, s) + + def add_value(self, value): + """add a parsed value according to arguments sequence""" + self._values_ori.append(value) + arguments = self.args.arguments + try: + arg_type, arg_name = arguments[self.args_idx] + except IndexError: + self.disp( + _("arguments in input data and in arguments sequence don't match"), + error=True, + ) + self.host.quit(C.EXIT_DATA_ERROR) + self.args_idx += 1 + while self.args_idx < len(arguments): + next_arg = arguments[self.args_idx] + if next_arg[0] not in OPT_TYPES: + # value will not be used if False or None, so we skip filter + if value not in (False, None): + # we have a filter + filter_type, filter_arg = arguments[self.args_idx] + value = self.filter(filter_type, filter_arg, value) + else: + break + self.args_idx += 1 + + if value is None: + # we ignore this argument + return + + if value is False: + # we skip the whole row + if self.args.debug: + self.disp( + A.color( + C.A_SUBHEADER, + _("values: "), + A.RESET, + ", ".join(self._values_ori), + ), + 2, + ) + self.disp(A.color(A.BOLD, _("**SKIPPING**\n"))) + self.reset() + self.idx += 1 + raise exceptions.CancelError + + if not isinstance(value, list): + value = [value] + + for v in value: + if arg_type == OPT_STDIN: + self._stdin.append(v) + elif arg_type == OPT_SHORT: + self._opts.append("-{}".format(arg_name)) + self._opts.append(v) + elif arg_type == OPT_LONG: + self._opts.append("--{}".format(arg_name)) + self._opts.append(v) + elif arg_type == OPT_POS: + self._pos.append(v) + elif arg_type == OPT_IGNORE: + pass + else: + self.parser.error( + _( + "Invalid argument, an option type is expected, got {type_}:{name}" + ).format(type_=arg_type, name=arg_name) + ) + + async def runCommand(self): + """run requested command with parsed arguments""" + if self.args_idx != len(self.args.arguments): + self.disp( + _("arguments in input data and in arguments sequence don't match"), + error=True, + ) + self.host.quit(C.EXIT_DATA_ERROR) + end = '\n' if self.args.debug else ' ' + self.disp( + A.color(C.A_HEADER, _("command {idx}").format(idx=self.idx)), + end = end, + ) + stdin = "".join(self._stdin) + if self.args.debug: + self.disp( + A.color( + C.A_SUBHEADER, + _("values: "), + A.RESET, + ", ".join([shlex.quote(a) for a in self._values_ori]) + ), + 2, + ) + + if stdin: + self.disp(A.color(C.A_SUBHEADER, "--- STDIN ---")) + self.disp(stdin) + self.disp(A.color(C.A_SUBHEADER, "-------------")) + + self.disp( + "{indent}{prog} {static} {options} {positionals}".format( + indent=4 * " ", + prog=sys.argv[0], + static=" ".join(self.args.command), + options=" ".join(shlex.quote(o) for o in self._opts), + positionals=" ".join(shlex.quote(p) for p in self._pos), + ) + ) + self.disp("\n") + else: + self.disp(" (" + ", ".join(self._values_ori) + ")", 2, end=' ') + args = [sys.argv[0]] + self.args.command + self._opts + self._pos + p = await asyncio.create_subprocess_exec( + *args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = await p.communicate(stdin.encode('utf-8')) + log = self.args.log + log_err = self.args.log_err + log_tpl = "{command}\n{buff}\n\n" + if log: + log.write(log_tpl.format( + command=" ".join(shlex.quote(a) for a in args), + buff=stdout.decode('utf-8', 'replace'))) + if log_err: + log_err.write(log_tpl.format( + command=" ".join(shlex.quote(a) for a in args), + buff=stderr.decode('utf-8', 'replace'))) + ret = p.returncode + if ret == 0: + self.disp(A.color(C.A_SUCCESS, _("OK"))) + else: + self.disp(A.color(C.A_FAILURE, _("FAILED"))) + + self.reset() + self.idx += 1 + + def filter(self, filter_type, filter_arg, value): + """change input value + + @param filter_type(unicode): name of the filter + @param filter_arg(unicode, None): argument of the filter + @param value(unicode): value to filter + @return (unicode, False, None): modified value + False to skip the whole row + None to ignore this argument (but continue row with other ones) + """ + raise NotImplementedError + + +class Csv(InputCommon): + def __init__(self, host): + super(Csv, self).__init__(host, "csv", _("comma-separated values")) + + def add_parser_options(self): + InputCommon.add_parser_options(self) + self.parser.add_argument( + "-r", + "--row", + type=int, + default=0, + help=_("starting row (previous ones will be ignored)"), + ) + self.parser.add_argument( + "-S", + "--split", + action="append_const", + const=("split", None), + dest="arguments", + help=_("split value in several options"), + ) + self.parser.add_argument( + "-E", + "--empty", + action="append", + type=self.opt("empty"), + dest="arguments", + help=_("action to do on empty value ({choices})").format( + choices=", ".join(OPT_EMPTY_CHOICES) + ), + ) + + def filter(self, filter_type, filter_arg, value): + if filter_type == "split": + return value.split() + elif filter_type == "empty": + if filter_arg == OPT_EMPTY_IGNORE: + return value if value else None + elif filter_arg == OPT_EMPTY_SKIP: + return value if value else False + else: + self.parser.error( + _("--empty value must be one of {choices}").format( + choices=", ".join(OPT_EMPTY_CHOICES) + ) + ) + + super(Csv, self).filter(filter_type, filter_arg, value) + + async def start(self): + import csv + + if self.args.encoding: + sys.stdin.reconfigure(encoding=self.args.encoding, errors="replace") + reader = csv.reader(sys.stdin) + for idx, row in enumerate(reader): + try: + if idx < self.args.row: + continue + for value in row: + self.add_value(value) + await self.runCommand() + except exceptions.CancelError: + #  this row has been cancelled, we skip it + continue + + self.host.quit() + + +class Input(base.CommandBase): + subcommands = (Csv,) + + def __init__(self, host): + super(Input, self).__init__( + host, + "input", + use_profile=False, + help=_("launch command with external input"), + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_invitation.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_invitation.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.tools.common import data_format + +__commands__ = ["Invitation"] + + +class Create(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "create", + use_profile=False, + use_output=C.OUTPUT_DICT, + help=_("create and send an invitation"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-j", + "--jid", + default="", + help="jid of the invitee (default: generate one)", + ) + self.parser.add_argument( + "-P", + "--password", + default="", + help="password of the invitee profile/XMPP account (default: generate one)", + ) + self.parser.add_argument( + "-n", + "--name", + default="", + help="name of the invitee", + ) + self.parser.add_argument( + "-N", + "--host-name", + default="", + help="name of the host", + ) + self.parser.add_argument( + "-e", + "--email", + action="append", + default=[], + help="email(s) to send the invitation to (if --no-email is set, email will just be saved)", + ) + self.parser.add_argument( + "--no-email", action="store_true", help="do NOT send invitation email" + ) + self.parser.add_argument( + "-l", + "--lang", + default="", + help="main language spoken by the invitee", + ) + self.parser.add_argument( + "-u", + "--url", + default="", + help="template to construct the URL", + ) + self.parser.add_argument( + "-s", + "--subject", + default="", + help="subject of the invitation email (default: generic subject)", + ) + self.parser.add_argument( + "-b", + "--body", + default="", + help="body of the invitation email (default: generic body)", + ) + self.parser.add_argument( + "-x", + "--extra", + metavar=("KEY", "VALUE"), + action="append", + nargs=2, + default=[], + help="extra data to associate with invitation/invitee", + ) + self.parser.add_argument( + "-p", + "--profile", + default="", + help="profile doing the invitation (default: don't associate profile)", + ) + + async def start(self): + extra = dict(self.args.extra) + email = self.args.email[0] if self.args.email else None + emails_extra = self.args.email[1:] + if self.args.no_email: + if email: + extra["email"] = email + data_format.iter2dict("emails_extra", emails_extra) + else: + if not email: + self.parser.error( + _("you need to specify an email address to send email invitation") + ) + + try: + invitation_data = await self.host.bridge.invitation_create( + email, + emails_extra, + self.args.jid, + self.args.password, + self.args.name, + self.args.host_name, + self.args.lang, + self.args.url, + self.args.subject, + self.args.body, + extra, + self.args.profile, + ) + except Exception as e: + self.disp(f"can't create invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(invitation_data) + self.host.quit(C.EXIT_OK) + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_profile=False, + use_output=C.OUTPUT_DICT, + help=_("get invitation data"), + ) + + def add_parser_options(self): + self.parser.add_argument("id", help=_("invitation UUID")) + self.parser.add_argument( + "-j", + "--with-jid", + action="store_true", + help=_("start profile session and retrieve jid"), + ) + + async def output_data(self, data, jid_=None): + if jid_ is not None: + data["jid"] = jid_ + await self.output(data) + self.host.quit() + + async def start(self): + try: + invitation_data = await self.host.bridge.invitation_get( + self.args.id, + ) + except Exception as e: + self.disp(msg=_("can't get invitation data: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if not self.args.with_jid: + await self.output_data(invitation_data) + else: + profile = invitation_data["guest_profile"] + try: + await self.host.bridge.profile_start_session( + invitation_data["password"], + profile, + ) + except Exception as e: + self.disp(msg=_("can't start session: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + jid_ = await self.host.bridge.param_get_a_async( + "JabberID", + "Connection", + profile_key=profile, + ) + except Exception as e: + self.disp(msg=_("can't retrieve jid: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + await self.output_data(invitation_data, jid_) + + +class Delete(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "delete", + use_profile=False, + help=_("delete guest account"), + ) + + def add_parser_options(self): + self.parser.add_argument("id", help=_("invitation UUID")) + + async def start(self): + try: + await self.host.bridge.invitation_delete( + self.args.id, + ) + except Exception as e: + self.disp(msg=_("can't delete guest account: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() + + +class Modify(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, host, "modify", use_profile=False, help=_("modify existing invitation") + ) + + def add_parser_options(self): + self.parser.add_argument( + "--replace", action="store_true", help="replace the whole data" + ) + self.parser.add_argument( + "-n", + "--name", + default="", + help="name of the invitee", + ) + self.parser.add_argument( + "-N", + "--host-name", + default="", + help="name of the host", + ) + self.parser.add_argument( + "-e", + "--email", + default="", + help="email to send the invitation to (if --no-email is set, email will just be saved)", + ) + self.parser.add_argument( + "-l", + "--lang", + dest="language", + default="", + help="main language spoken by the invitee", + ) + self.parser.add_argument( + "-x", + "--extra", + metavar=("KEY", "VALUE"), + action="append", + nargs=2, + default=[], + help="extra data to associate with invitation/invitee", + ) + self.parser.add_argument( + "-p", + "--profile", + default="", + help="profile doing the invitation (default: don't associate profile", + ) + self.parser.add_argument("id", help=_("invitation UUID")) + + async def start(self): + extra = dict(self.args.extra) + for arg_name in ("name", "host_name", "email", "language", "profile"): + value = getattr(self.args, arg_name) + if not value: + continue + if arg_name in extra: + self.parser.error( + _( + "you can't set {arg_name} in both optional argument and extra" + ).format(arg_name=arg_name) + ) + extra[arg_name] = value + try: + await self.host.bridge.invitation_modify( + self.args.id, + extra, + self.args.replace, + ) + except Exception as e: + self.disp(f"can't modify invitation: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("invitations have been modified successfuly")) + self.host.quit(C.EXIT_OK) + + +class List(base.CommandBase): + def __init__(self, host): + extra_outputs = {"default": self.default_output} + base.CommandBase.__init__( + self, + host, + "list", + use_profile=False, + use_output=C.OUTPUT_COMPLEX, + extra_outputs=extra_outputs, + help=_("list invitations data"), + ) + + def default_output(self, data): + for idx, datum in enumerate(data.items()): + if idx: + self.disp("\n") + key, invitation_data = datum + self.disp(A.color(C.A_HEADER, key)) + indent = " " + for k, v in invitation_data.items(): + self.disp(indent + A.color(C.A_SUBHEADER, k + ":") + " " + str(v)) + + def add_parser_options(self): + self.parser.add_argument( + "-p", + "--profile", + default=C.PROF_KEY_NONE, + help=_("return only invitations linked to this profile"), + ) + + async def start(self): + try: + data = await self.host.bridge.invitation_list( + self.args.profile, + ) + except Exception as e: + self.disp(f"return only invitations linked to this profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(data) + self.host.quit() + + +class Invitation(base.CommandBase): + subcommands = (Create, Get, Delete, Modify, List) + + def __init__(self, host): + super(Invitation, self).__init__( + host, + "invitation", + use_profile=False, + help=_("invitation of user(s) without XMPP account"), + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_list.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import json +import os +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp import common +from libervia.frontends.jp.constants import Const as C +from . import base + +__commands__ = ["List"] + +FIELDS_MAP = "mapping" + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_verbose=True, + use_pubsub=True, + pubsub_flags={C.MULTI_ITEMS}, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + use_output=C.OUTPUT_LIST_XMLUI, + help=_("get lists"), + ) + + def add_parser_options(self): + pass + + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) + try: + lists_data = data_format.deserialise( + await self.host.bridge.list_get( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + self.get_pubsub_extra(), + self.profile, + ), + type_check=list, + ) + except Exception as e: + self.disp(f"can't get lists: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(lists_data[0]) + self.host.quit(C.EXIT_OK) + + +class Set(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + help=_("set a list item"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--field", + action="append", + nargs="+", + dest="fields", + required=True, + metavar=("NAME", "VALUES"), + help=_("field(s) to set (required)"), + ) + self.parser.add_argument( + "-U", + "--update", + choices=("auto", "true", "false"), + default="auto", + help=_("update existing item instead of replacing it (DEFAULT: auto)"), + ) + self.parser.add_argument( + "item", + nargs="?", + default="", + help=_("id, URL of the item to update, or nothing for new item"), + ) + + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) + if self.args.update == "auto": + # we update if we have a item id specified + update = bool(self.args.item) + else: + update = C.bool(self.args.update) + + values = {} + + for field_data in self.args.fields: + values.setdefault(field_data[0], []).extend(field_data[1:]) + + extra = {"update": update} + + try: + item_id = await self.host.bridge.list_set( + self.args.service, + self.args.node, + values, + "", + self.args.item, + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't set list item: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(f"item {str(item_id or self.args.item)!r} set successfully") + self.host.quit(C.EXIT_OK) + + +class Delete(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "delete", + use_pubsub=True, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + help=_("delete a list item"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", "--force", action="store_true", help=_("delete without confirmation") + ) + self.parser.add_argument( + "-N", "--notify", action="store_true", help=_("notify deletion") + ) + self.parser.add_argument( + "item", + help=_("id of the item to delete"), + ) + + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) + if not self.args.item: + self.parser.error(_("You need to specify a list item to delete")) + if not self.args.force: + message = _("Are you sure to delete list item {item_id} ?").format( + item_id=self.args.item + ) + await self.host.confirm_or_quit(message, _("item deletion cancelled")) + try: + await self.host.bridge.list_delete_item( + self.args.service, + self.args.node, + self.args.item, + self.args.notify, + self.profile, + ) + except Exception as e: + self.disp(_("can't delete item: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("item {item} has been deleted").format(item=self.args.item)) + self.host.quit(C.EXIT_OK) + + +class Import(base.CommandBase): + # TODO: factorize with blog/import + + def __init__(self, host): + super().__init__( + host, + "import", + use_progress=True, + use_verbose=True, + help=_("import tickets from external software/dataset"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "importer", + nargs="?", + help=_("importer name, nothing to display importers list"), + ) + self.parser.add_argument( + "-o", + "--option", + action="append", + nargs=2, + default=[], + metavar=("NAME", "VALUE"), + help=_("importer specific options (see importer description)"), + ) + self.parser.add_argument( + "-m", + "--map", + action="append", + nargs=2, + default=[], + metavar=("IMPORTED_FIELD", "DEST_FIELD"), + help=_( + "specified field in import data will be put in dest field (default: use " + "same field name, or ignore if it doesn't exist)" + ), + ) + self.parser.add_argument( + "-s", + "--service", + default="", + metavar="PUBSUB_SERVICE", + help=_("PubSub service where the items must be uploaded (default: server)"), + ) + self.parser.add_argument( + "-n", + "--node", + default="", + metavar="PUBSUB_NODE", + help=_( + "PubSub node where the items must be uploaded (default: tickets' " + "defaults)" + ), + ) + self.parser.add_argument( + "location", + nargs="?", + help=_( + "importer data location (see importer description), nothing to show " + "importer description" + ), + ) + + async def on_progress_started(self, metadata): + self.disp(_("Tickets upload started"), 2) + + async def on_progress_finished(self, metadata): + self.disp(_("Tickets uploaded successfully"), 2) + + async def on_progress_error(self, error_msg): + self.disp( + _("Error while uploading tickets: {error_msg}").format(error_msg=error_msg), + error=True, + ) + + async def start(self): + if self.args.location is None: + # no location, the list of importer or description is requested + for name in ("option", "service", "node"): + if getattr(self.args, name): + self.parser.error( + _( + "{name} argument can't be used without location argument" + ).format(name=name) + ) + if self.args.importer is None: + self.disp( + "\n".join( + [ + f"{name}: {desc}" + for name, desc in await self.host.bridge.ticketsImportList() + ] + ) + ) + else: + try: + short_desc, long_desc = await self.host.bridge.ticketsImportDesc( + self.args.importer + ) + except Exception as e: + self.disp(f"can't get importer description: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(f"{name}: {short_desc}\n\n{long_desc}") + self.host.quit() + else: + # we have a location, an import is requested + + if self.args.progress: + # we use a custom progress bar template as we want a counter + self.pbar_template = [ + _("Progress: "), + ["Percentage"], + " ", + ["Bar"], + " ", + ["Counter"], + " ", + ["ETA"], + ] + + options = {key: value for key, value in self.args.option} + fields_map = dict(self.args.map) + if fields_map: + if FIELDS_MAP in options: + self.parser.error( + _( + "fields_map must be specified either preencoded in --option or " + "using --map, but not both at the same time" + ) + ) + options[FIELDS_MAP] = json.dumps(fields_map) + + try: + progress_id = await self.host.bridge.ticketsImport( + self.args.importer, + self.args.location, + options, + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp( + _("Error while trying to import tickets: {e}").format(e=e), + error=True, + ) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.set_progress_id(progress_id) + + +class List(base.CommandBase): + subcommands = (Get, Set, Delete, Import) + + def __init__(self, host): + super(List, self).__init__( + host, "list", use_profile=False, help=_("pubsub lists handling") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_merge_request.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_merge_request.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import os.path +from . import base +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp import xmlui_manager +from libervia.frontends.jp import common + +__commands__ = ["MergeRequest"] + + +class Set(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + help=_("publish or update a merge request"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-i", + "--item", + default="", + help=_("id or URL of the request to update, or nothing for a new one"), + ) + self.parser.add_argument( + "-r", + "--repository", + metavar="PATH", + default=".", + help=_("path of the repository (DEFAULT: current directory)"), + ) + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("publish merge request without confirmation"), + ) + self.parser.add_argument( + "-l", + "--label", + dest="labels", + action="append", + help=_("labels to categorize your request"), + ) + + async def start(self): + self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) + await common.fill_well_known_uri(self, self.repository, "merge requests") + if not self.args.force: + message = _( + "You are going to publish your changes to service " + "[{service}], are you sure ?" + ).format(service=self.args.service) + await self.host.confirm_or_quit( + message, _("merge request publication cancelled") + ) + + extra = {"update": True} if self.args.item else {} + values = {} + if self.args.labels is not None: + values["labels"] = self.args.labels + try: + published_id = await self.host.bridge.merge_request_set( + self.args.service, + self.args.node, + self.repository, + "auto", + values, + "", + self.args.item, + data_format.serialise(extra), + self.profile, + ) + except Exception as e: + self.disp(f"can't create merge requests: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if published_id: + self.disp( + _("Merge request published at {published_id}").format( + published_id=published_id + ) + ) + else: + self.disp(_("Merge request published")) + + self.host.quit(C.EXIT_OK) + + +class Get(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_verbose=True, + use_pubsub=True, + pubsub_flags={C.MULTI_ITEMS}, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + help=_("get a merge request"), + ) + + def add_parser_options(self): + pass + + async def start(self): + await common.fill_well_known_uri(self, os.getcwd(), "merge requests", meta_map={}) + extra = {} + try: + requests_data = data_format.deserialise( + await self.host.bridge.merge_requests_get( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + "", + data_format.serialise(extra), + self.profile, + ) + ) + except Exception as e: + self.disp(f"can't get merge request: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if self.verbosity >= 1: + whitelist = None + else: + whitelist = {"id", "title", "body"} + for request_xmlui in requests_data["items"]: + xmlui = xmlui_manager.create(self.host, request_xmlui, whitelist=whitelist) + await xmlui.show(values_only=True) + self.disp("") + self.host.quit(C.EXIT_OK) + + +class Import(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "import", + use_pubsub=True, + pubsub_flags={C.SINGLE_ITEM, C.ITEM}, + pubsub_defaults={"service": _("auto"), "node": _("auto")}, + help=_("import a merge request"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-r", + "--repository", + metavar="PATH", + default=".", + help=_("path of the repository (DEFAULT: current directory)"), + ) + + async def start(self): + self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) + await common.fill_well_known_uri( + self, self.repository, "merge requests", meta_map={} + ) + extra = {} + try: + await self.host.bridge.merge_requests_import( + self.repository, + self.args.item, + self.args.service, + self.args.node, + extra, + self.profile, + ) + except Exception as e: + self.disp(f"can't import merge request: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class MergeRequest(base.CommandBase): + subcommands = (Set, Get, Import) + + def __init__(self, host): + super(MergeRequest, self).__init__( + host, "merge-request", use_profile=False, help=_("merge-request management") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_message.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_message.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from pathlib import Path +import sys + +from twisted.python import filepath + +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.tools.utils import clean_ustr +from libervia.frontends.jp import base +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.tools import jid + + +__commands__ = ["Message"] + + +class Send(base.CommandBase): + def __init__(self, host): + super(Send, self).__init__(host, "send", help=_("send a message to a contact")) + + def add_parser_options(self): + self.parser.add_argument( + "-l", "--lang", type=str, default="", help=_("language of the message") + ) + self.parser.add_argument( + "-s", + "--separate", + action="store_true", + help=_( + "separate xmpp messages: send one message per line instead of one " + "message alone." + ), + ) + self.parser.add_argument( + "-n", + "--new-line", + action="store_true", + help=_( + "add a new line at the beginning of the input" + ), + ) + self.parser.add_argument( + "-S", + "--subject", + help=_("subject of the message"), + ) + self.parser.add_argument( + "-L", "--subject-lang", type=str, default="", help=_("language of subject") + ) + self.parser.add_argument( + "-t", + "--type", + choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,), + default=C.MESS_TYPE_AUTO, + help=_("type of the message"), + ) + self.parser.add_argument("-e", "--encrypt", metavar="ALGORITHM", + help=_("encrypt message using given algorithm")) + self.parser.add_argument( + "--encrypt-noreplace", + action="store_true", + help=_("don't replace encryption algorithm if an other one is already used")) + self.parser.add_argument( + "-a", "--attach", dest="attachments", action="append", metavar="FILE_PATH", + help=_("add a file as an attachment") + ) + syntax = self.parser.add_mutually_exclusive_group() + syntax.add_argument("-x", "--xhtml", action="store_true", help=_("XHTML body")) + syntax.add_argument("-r", "--rich", action="store_true", help=_("rich body")) + self.parser.add_argument( + "jid", help=_("the destination jid") + ) + + async def send_stdin(self, dest_jid): + """Send incomming data on stdin to jabber contact + + @param dest_jid: destination jid + """ + header = "\n" if self.args.new_line else "" + # FIXME: stdin is not read asynchronously at the moment + stdin_lines = [ + stream for stream in sys.stdin.readlines() + ] + extra = {} + if self.args.subject is None: + subject = {} + else: + subject = {self.args.subject_lang: self.args.subject} + + if self.args.xhtml or self.args.rich: + key = "xhtml" if self.args.xhtml else "rich" + if self.args.lang: + key = f"{key}_{self.args.lang}" + extra[key] = clean_ustr("".join(stdin_lines)) + stdin_lines = [] + + to_send = [] + + error = False + + if self.args.separate: + # we send stdin in several messages + if header: + # first we sent the header + try: + await self.host.bridge.message_send( + dest_jid, + {self.args.lang: header}, + subject, + self.args.type, + profile_key=self.profile, + ) + except Exception as e: + self.disp(f"can't send header: {e}", error=True) + error = True + + to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))} + for l in stdin_lines) + else: + # we sent all in a single message + if not (self.args.xhtml or self.args.rich): + msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))} + else: + msg = {} + to_send.append(msg) + + if self.args.attachments: + attachments = extra[C.KEY_ATTACHMENTS] = [] + for attachment in self.args.attachments: + try: + file_path = str(Path(attachment).resolve(strict=True)) + except FileNotFoundError: + self.disp("file {attachment} doesn't exists, ignoring", error=True) + else: + attachments.append({"path": file_path}) + + for idx, msg in enumerate(to_send): + if idx > 0 and C.KEY_ATTACHMENTS in extra: + # if we send several messages, we only want to send attachments with the + # first one + del extra[C.KEY_ATTACHMENTS] + try: + await self.host.bridge.message_send( + dest_jid, + msg, + subject, + self.args.type, + data_format.serialise(extra), + profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't send message {msg!r}: {e}", error=True) + error = True + + if error: + # at least one message sending failed + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + self.host.quit() + + async def start(self): + if self.args.xhtml and self.args.separate: + self.disp( + "argument -s/--separate is not compatible yet with argument -x/--xhtml", + error=True, + ) + self.host.quit(C.EXIT_BAD_ARG) + + jids = await self.host.check_jids([self.args.jid]) + jid_ = jids[0] + + if self.args.encrypt_noreplace and self.args.encrypt is None: + self.parser.error("You need to use --encrypt if you use --encrypt-noreplace") + + if self.args.encrypt is not None: + try: + namespace = await self.host.bridge.encryption_namespace_get( + self.args.encrypt) + except Exception as e: + self.disp(f"can't get encryption namespace: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.message_encryption_start( + jid_, namespace, not self.args.encrypt_noreplace, self.profile + ) + except Exception as e: + self.disp(f"can't start encryption session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + await self.send_stdin(jid_) + + +class Retract(base.CommandBase): + + def __init__(self, host): + super().__init__(host, "retract", help=_("retract a message")) + + def add_parser_options(self): + self.parser.add_argument( + "message_id", + help=_("ID of the message (internal ID)") + ) + + async def start(self): + try: + await self.host.bridge.message_retract( + self.args.message_id, + self.profile + ) + except Exception as e: + self.disp(f"can't retract message: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp( + "message retraction has been requested, please note that this is a " + "request which can't be enforced (see documentation for details).") + self.host.quit(C.EXIT_OK) + + +class MAM(base.CommandBase): + + def __init__(self, host): + super(MAM, self).__init__( + host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, + help=_("query archives using MAM")) + + def add_parser_options(self): + self.parser.add_argument( + "-s", "--service", default="", + help=_("jid of the service (default: profile's server")) + self.parser.add_argument( + "-S", "--start", dest="mam_start", type=base.date_decoder, + help=_( + "start fetching archive from this date (default: from the beginning)")) + self.parser.add_argument( + "-E", "--end", dest="mam_end", type=base.date_decoder, + help=_("end fetching archive after this date (default: no limit)")) + self.parser.add_argument( + "-W", "--with", dest="mam_with", + help=_("retrieve only archives with this jid")) + self.parser.add_argument( + "-m", "--max", dest="rsm_max", type=int, default=20, + help=_("maximum number of items to retrieve, using RSM (default: 20))")) + rsm_page_group = self.parser.add_mutually_exclusive_group() + rsm_page_group.add_argument( + "-a", "--after", dest="rsm_after", + help=_("find page after this item"), metavar='ITEM_ID') + rsm_page_group.add_argument( + "-b", "--before", dest="rsm_before", + help=_("find page before this item"), metavar='ITEM_ID') + rsm_page_group.add_argument( + "--index", dest="rsm_index", type=int, + help=_("index of the page to retrieve")) + + async def start(self): + extra = {} + if self.args.mam_start is not None: + extra["mam_start"] = float(self.args.mam_start) + if self.args.mam_end is not None: + extra["mam_end"] = float(self.args.mam_end) + if self.args.mam_with is not None: + extra["mam_with"] = self.args.mam_with + for suff in ('max', 'after', 'before', 'index'): + key = 'rsm_' + suff + value = getattr(self.args,key) + if value is not None: + extra[key] = str(value) + try: + data, metadata_s, profile = await self.host.bridge.mam_get( + self.args.service, data_format.serialise(extra), self.profile) + except Exception as e: + self.disp(f"can't retrieve MAM archives: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + metadata = data_format.deserialise(metadata_s) + + try: + session_info = await self.host.bridge.session_infos_get(self.profile) + except Exception as e: + self.disp(f"can't get session infos: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + # we need to fill own_jid for message output + self.host.own_jid = jid.JID(session_info["jid"]) + + await self.output(data) + + # FIXME: metadata are not displayed correctly and don't play nice with output + # they should be added to output data somehow + if self.verbosity: + for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count", + "mam_complete", "mam_stable"): + if value in metadata: + label = value.split("_")[1] + self.disp(A.color( + C.A_HEADER, label, ': ' , A.RESET, metadata[value])) + + self.host.quit() + + +class Message(base.CommandBase): + subcommands = (Send, Retract, MAM) + + def __init__(self, host): + super(Message, self).__init__( + host, "message", use_profile=False, help=_("messages handling") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_param.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_param.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from . import base +from libervia.backend.core.i18n import _ +from .constants import Const as C + +__commands__ = ["Param"] + + +class Get(base.CommandBase): + def __init__(self, host): + super(Get, self).__init__( + host, "get", need_connect=False, help=_("get a parameter value") + ) + + def add_parser_options(self): + self.parser.add_argument( + "category", nargs="?", help=_("category of the parameter") + ) + self.parser.add_argument("name", nargs="?", help=_("name of the parameter")) + self.parser.add_argument( + "-a", + "--attribute", + type=str, + default="value", + help=_("name of the attribute to get"), + ) + self.parser.add_argument( + "--security-limit", type=int, default=-1, help=_("security limit") + ) + + async def start(self): + if self.args.category is None: + categories = await self.host.bridge.params_categories_get() + print("\n".join(categories)) + elif self.args.name is None: + try: + values_dict = await self.host.bridge.params_values_from_category_get_async( + self.args.category, self.args.security_limit, "", "", self.profile + ) + except Exception as e: + self.disp( + _("can't find requested parameters: {e}").format(e=e), error=True + ) + self.host.quit(C.EXIT_NOT_FOUND) + else: + for name, value in values_dict.items(): + print(f"{name}\t{value}") + else: + try: + value = await self.host.bridge.param_get_a_async( + self.args.name, + self.args.category, + self.args.attribute, + self.args.security_limit, + self.profile, + ) + except Exception as e: + self.disp( + _("can't find requested parameter: {e}").format(e=e), error=True + ) + self.host.quit(C.EXIT_NOT_FOUND) + else: + print(value) + self.host.quit() + + +class Set(base.CommandBase): + def __init__(self, host): + super(Set, self).__init__( + host, "set", need_connect=False, help=_("set a parameter value") + ) + + def add_parser_options(self): + self.parser.add_argument("category", help=_("category of the parameter")) + self.parser.add_argument("name", help=_("name of the parameter")) + self.parser.add_argument("value", help=_("name of the parameter")) + self.parser.add_argument( + "--security-limit", type=int, default=-1, help=_("security limit") + ) + + async def start(self): + try: + await self.host.bridge.param_set( + self.args.name, + self.args.value, + self.args.category, + self.args.security_limit, + self.profile, + ) + except Exception as e: + self.disp(_("can't set requested parameter: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class SaveTemplate(base.CommandBase): + # FIXME: this should probably be removed, it's not used and not useful for end-user + + def __init__(self, host): + super(SaveTemplate, self).__init__( + host, + "save", + use_profile=False, + help=_("save parameters template to xml file"), + ) + + def add_parser_options(self): + self.parser.add_argument("filename", type=str, help=_("output file")) + + async def start(self): + """Save parameters template to XML file""" + try: + await self.host.bridge.params_template_save(self.args.filename) + except Exception as e: + self.disp(_("can't save parameters to file: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp( + _("parameters saved to file {filename}").format( + filename=self.args.filename + ) + ) + self.host.quit() + + +class LoadTemplate(base.CommandBase): + # FIXME: this should probably be removed, it's not used and not useful for end-user + + def __init__(self, host): + super(LoadTemplate, self).__init__( + host, + "load", + use_profile=False, + help=_("load parameters template from xml file"), + ) + + def add_parser_options(self): + self.parser.add_argument("filename", type=str, help=_("input file")) + + async def start(self): + """Load parameters template from xml file""" + try: + self.host.bridge.params_template_load(self.args.filename) + except Exception as e: + self.disp(_("can't load parameters from file: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp( + _("parameters loaded from file {filename}").format( + filename=self.args.filename + ) + ) + self.host.quit() + + +class Param(base.CommandBase): + subcommands = (Get, Set, SaveTemplate, LoadTemplate) + + def __init__(self, host): + super(Param, self).__init__( + host, "param", use_profile=False, help=_("Save/load parameters template") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_ping.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_ping.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from . import base +from libervia.backend.core.i18n import _ +from libervia.frontends.jp.constants import Const as C + +__commands__ = ["Ping"] + + +class Ping(base.CommandBase): + def __init__(self, host): + super(Ping, self).__init__(host, "ping", help=_("ping XMPP entity")) + + def add_parser_options(self): + self.parser.add_argument("jid", help=_("jid to ping")) + self.parser.add_argument( + "-d", "--delay-only", action="store_true", help=_("output delay only (in s)") + ) + + async def start(self): + try: + pong_time = await self.host.bridge.ping(self.args.jid, self.profile) + except Exception as e: + self.disp(msg=_("can't do the ping: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + msg = pong_time if self.args.delay_only else f"PONG ({pong_time} s)" + self.disp(msg) + self.host.quit() diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_pipe.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_pipe.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +import errno +from functools import partial +import socket +import sys + +from libervia.backend.core.i18n import _ +from libervia.backend.tools.common import data_format +from libervia.frontends.jp import base +from libervia.frontends.jp import xmlui_manager +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.tools import jid + +__commands__ = ["Pipe"] + +START_PORT = 9999 + + +class PipeOut(base.CommandBase): + def __init__(self, host): + super(PipeOut, self).__init__(host, "out", help=_("send a pipe a stream")) + + def add_parser_options(self): + self.parser.add_argument( + "jid", help=_("the destination jid") + ) + + async def start(self): + """ Create named pipe, and send stdin to it """ + try: + port = await self.host.bridge.stream_out( + await self.host.get_full_jid(self.args.jid), + self.profile, + ) + except Exception as e: + self.disp(f"can't start stream: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + # FIXME: we use temporarily blocking code here, as it simplify + # asyncio port: "loop.connect_read_pipe(lambda: reader_protocol, + # sys.stdin.buffer)" doesn't work properly when a file is piped in + # (we get a "ValueError: Pipe transport is for pipes/sockets only.") + # while it's working well for simple text sending. + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("127.0.0.1", int(port))) + + while True: + buf = sys.stdin.buffer.read(4096) + if not buf: + break + try: + s.sendall(buf) + except socket.error as e: + if e.errno == errno.EPIPE: + sys.stderr.write(f"e\n") + self.host.quit(1) + else: + raise e + self.host.quit() + + +async def handle_stream_in(reader, writer, host): + """Write all received data to stdout""" + while True: + data = await reader.read(4096) + if not data: + break + sys.stdout.buffer.write(data) + try: + sys.stdout.flush() + except IOError as e: + sys.stderr.write(f"{e}\n") + break + host.quit_from_signal() + + +class PipeIn(base.CommandAnswering): + def __init__(self, host): + super(PipeIn, self).__init__(host, "in", help=_("receive a pipe stream")) + self.action_callbacks = {"STREAM": self.on_stream_action} + + def add_parser_options(self): + self.parser.add_argument( + "jids", + nargs="*", + help=_('Jids accepted (none means "accept everything")'), + ) + + def get_xmlui_id(self, action_data): + try: + xml_ui = action_data["xmlui"] + except KeyError: + self.disp(_("Action has no XMLUI"), 1) + else: + ui = xmlui_manager.create(self.host, xml_ui) + if not ui.submit_id: + self.disp(_("Invalid XMLUI received"), error=True) + self.quit_from_signal(C.EXIT_INTERNAL_ERROR) + return ui.submit_id + + async def on_stream_action(self, action_data, action_id, security_limit, profile): + xmlui_id = self.get_xmlui_id(action_data) + if xmlui_id is None: + self.host.quit_from_signal(C.EXIT_ERROR) + try: + from_jid = jid.JID(action_data["from_jid"]) + except KeyError: + self.disp(_("Ignoring action without from_jid data"), error=True) + return + + if not self.bare_jids or from_jid.bare in self.bare_jids: + host, port = "localhost", START_PORT + while True: + try: + server = await asyncio.start_server( + partial(handle_stream_in, host=self.host), host, port) + except socket.error as e: + if e.errno == errno.EADDRINUSE: + port += 1 + else: + raise e + else: + break + xmlui_data = {"answer": C.BOOL_TRUE, "port": str(port)} + await self.host.bridge.action_launch( + xmlui_id, data_format.serialise(xmlui_data), profile_key=profile + ) + async with server: + await server.serve_forever() + self.host.quit_from_signal() + + async def start(self): + self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] + await self.start_answering() + + +class Pipe(base.CommandBase): + subcommands = (PipeOut, PipeIn) + + def __init__(self, host): + super(Pipe, self).__init__( + host, "pipe", use_profile=False, help=_("stream piping through XMPP") + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_profile.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_profile.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 + + +# jp: a SAT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This module permits to manage profiles. It can list, create, delete +and retrieve information about a profile.""" + +from libervia.frontends.jp.constants import Const as C +from libervia.backend.core.log import getLogger +from libervia.backend.core.i18n import _ +from libervia.frontends.jp import base + +log = getLogger(__name__) + + +__commands__ = ["Profile"] + +PROFILE_HELP = _('The name of the profile') + + +class ProfileConnect(base.CommandBase): + """Dummy command to use profile_session parent, i.e. to be able to connect without doing anything else""" + + def __init__(self, host): + # it's weird to have a command named "connect" with need_connect=False, but it can be handy to be able + # to launch just the session, so some paradoxes don't hurt + super(ProfileConnect, self).__init__(host, 'connect', need_connect=False, help=('connect a profile')) + + def add_parser_options(self): + pass + + async def start(self): + # connection is already managed by profile common commands + # so we just need to check arguments and quit + if not self.args.connect and not self.args.start_session: + self.parser.error(_("You need to use either --connect or --start-session")) + self.host.quit() + +class ProfileDisconnect(base.CommandBase): + + def __init__(self, host): + super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=('disconnect a profile')) + + def add_parser_options(self): + pass + + async def start(self): + try: + await self.host.bridge.disconnect(self.args.profile) + except Exception as e: + self.disp(f"can't disconnect profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.host.quit() + + +class ProfileCreate(base.CommandBase): + def __init__(self, host): + super(ProfileCreate, self).__init__( + host, 'create', use_profile=False, help=('create a new profile')) + + def add_parser_options(self): + self.parser.add_argument('profile', type=str, help=_('the name of the profile')) + self.parser.add_argument( + '-p', '--password', type=str, default='', + help=_('the password of the profile')) + self.parser.add_argument( + '-j', '--jid', type=str, help=_('the jid of the profile')) + self.parser.add_argument( + '-x', '--xmpp-password', type=str, + help=_( + 'the password of the XMPP account (use profile password if not specified)' + ), + metavar='PASSWORD') + self.parser.add_argument( + '-A', '--autoconnect', choices=[C.BOOL_TRUE, C.BOOL_FALSE], nargs='?', + const=C.BOOL_TRUE, + help=_('connect this profile automatically when backend starts') + ) + self.parser.add_argument( + '-C', '--component', default='', + help=_('set to component import name (entry point) if this is a component')) + + async def start(self): + """Create a new profile""" + if self.args.profile in await self.host.bridge.profiles_list_get(): + self.disp(f"Profile {self.args.profile} already exists.", error=True) + self.host.quit(C.EXIT_BRIDGE_ERROR) + try: + await self.host.bridge.profile_create( + self.args.profile, self.args.password, self.args.component) + except Exception as e: + self.disp(f"can't create profile: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + try: + await self.host.bridge.profile_start_session( + self.args.password, self.args.profile) + except Exception as e: + self.disp(f"can't start profile session: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if self.args.jid: + await self.host.bridge.param_set( + "JabberID", self.args.jid, "Connection", profile_key=self.args.profile) + xmpp_pwd = self.args.password or self.args.xmpp_password + if xmpp_pwd: + await self.host.bridge.param_set( + "Password", xmpp_pwd, "Connection", profile_key=self.args.profile) + + if self.args.autoconnect is not None: + await self.host.bridge.param_set( + "autoconnect_backend", self.args.autoconnect, "Connection", + profile_key=self.args.profile) + + self.disp(f'profile {self.args.profile} created successfully', 1) + self.host.quit() + + +class ProfileDefault(base.CommandBase): + def __init__(self, host): + super(ProfileDefault, self).__init__( + host, 'default', use_profile=False, help=('print default profile')) + + def add_parser_options(self): + pass + + async def start(self): + print(await self.host.bridge.profile_name_get('@DEFAULT@')) + self.host.quit() + + +class ProfileDelete(base.CommandBase): + def __init__(self, host): + super(ProfileDelete, self).__init__(host, 'delete', use_profile=False, help=('delete a profile')) + + def add_parser_options(self): + self.parser.add_argument('profile', type=str, help=PROFILE_HELP) + self.parser.add_argument('-f', '--force', action='store_true', help=_('delete profile without confirmation')) + + async def start(self): + if self.args.profile not in await self.host.bridge.profiles_list_get(): + log.error(f"Profile {self.args.profile} doesn't exist.") + self.host.quit(C.EXIT_NOT_FOUND) + if not self.args.force: + message = f"Are you sure to delete profile [{self.args.profile}] ?" + cancel_message = "Profile deletion cancelled" + await self.host.confirm_or_quit(message, cancel_message) + + await self.host.bridge.profile_delete_async(self.args.profile) + self.host.quit() + + +class ProfileInfo(base.CommandBase): + + def __init__(self, host): + super(ProfileInfo, self).__init__( + host, 'info', need_connect=False, use_output=C.OUTPUT_DICT, + help=_('get information about a profile')) + self.to_show = [(_("jid"), "Connection", "JabberID"),] + + def add_parser_options(self): + self.parser.add_argument( + '--show-password', action='store_true', + help=_('show the XMPP password IN CLEAR TEXT')) + + async def start(self): + if self.args.show_password: + self.to_show.append((_("XMPP password"), "Connection", "Password")) + self.to_show.append((_("autoconnect (backend)"), "Connection", + "autoconnect_backend")) + data = {} + for label, category, name in self.to_show: + try: + value = await self.host.bridge.param_get_a_async( + name, category, profile_key=self.host.profile) + except Exception as e: + self.disp(f"can't get {name}/{category} param: {e}", error=True) + else: + data[label] = value + + await self.output(data) + self.host.quit() + + +class ProfileList(base.CommandBase): + def __init__(self, host): + super(ProfileList, self).__init__( + host, 'list', use_profile=False, use_output='list', help=('list profiles')) + + def add_parser_options(self): + group = self.parser.add_mutually_exclusive_group() + group.add_argument( + '-c', '--clients', action='store_true', help=_('get clients profiles only')) + group.add_argument( + '-C', '--components', action='store_true', + help=('get components profiles only')) + + async def start(self): + if self.args.clients: + clients, components = True, False + elif self.args.components: + clients, components = False, True + else: + clients, components = True, True + await self.output(await self.host.bridge.profiles_list_get(clients, components)) + self.host.quit() + + +class ProfileModify(base.CommandBase): + + def __init__(self, host): + super(ProfileModify, self).__init__( + host, 'modify', need_connect=False, help=_('modify an existing profile')) + + def add_parser_options(self): + profile_pwd_group = self.parser.add_mutually_exclusive_group() + profile_pwd_group.add_argument( + '-w', '--password', help=_('change the password of the profile')) + profile_pwd_group.add_argument( + '--disable-password', action='store_true', + help=_('disable profile password (dangerous!)')) + self.parser.add_argument('-j', '--jid', help=_('the jid of the profile')) + self.parser.add_argument( + '-x', '--xmpp-password', help=_('change the password of the XMPP account'), + metavar='PASSWORD') + self.parser.add_argument( + '-D', '--default', action='store_true', help=_('set as default profile')) + self.parser.add_argument( + '-A', '--autoconnect', choices=[C.BOOL_TRUE, C.BOOL_FALSE], nargs='?', + const=C.BOOL_TRUE, + help=_('connect this profile automatically when backend starts') + ) + + async def start(self): + if self.args.disable_password: + self.args.password = '' + if self.args.password is not None: + await self.host.bridge.param_set( + "Password", self.args.password, "General", profile_key=self.host.profile) + if self.args.jid is not None: + await self.host.bridge.param_set( + "JabberID", self.args.jid, "Connection", profile_key=self.host.profile) + if self.args.xmpp_password is not None: + await self.host.bridge.param_set( + "Password", self.args.xmpp_password, "Connection", + profile_key=self.host.profile) + if self.args.default: + await self.host.bridge.profile_set_default(self.host.profile) + if self.args.autoconnect is not None: + await self.host.bridge.param_set( + "autoconnect_backend", self.args.autoconnect, "Connection", + profile_key=self.host.profile) + + self.host.quit() + + +class Profile(base.CommandBase): + subcommands = ( + ProfileConnect, ProfileDisconnect, ProfileCreate, ProfileDefault, ProfileDelete, + ProfileInfo, ProfileList, ProfileModify) + + def __init__(self, host): + super(Profile, self).__init__( + host, 'profile', use_profile=False, help=_('profile commands')) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/jp/cmd_pubsub.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/jp/cmd_pubsub.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,3030 @@ +#!/usr/bin/env python3 + + +# jp: a SàT command line tool +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import argparse +import os.path +import re +import sys +import subprocess +import asyncio +import json +from . import base +from libervia.backend.core.i18n import _ +from libervia.backend.core import exceptions +from libervia.frontends.jp.constants import Const as C +from libervia.frontends.jp import common +from libervia.frontends.jp import arg_tools +from libervia.frontends.jp import xml_tools +from functools import partial +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common import uri +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.tools.common import date_utils +from libervia.frontends.tools import jid, strings +from libervia.frontends.bridge.bridge_frontend import BridgeException + +__commands__ = ["Pubsub"] + +PUBSUB_TMP_DIR = "pubsub" +PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema" +ALLOWED_SUBSCRIPTIONS_OWNER = ("subscribed", "pending", "none") + +# TODO: need to split this class in several modules, plugin should handle subcommands + + +class NodeInfo(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "info", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("retrieve node configuration"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-k", + "--key", + action="append", + dest="keys", + help=_("data key to filter"), + ) + + def remove_prefix(self, key): + return key[7:] if key.startswith("pubsub#") else key + + def filter_key(self, key): + return any((key == k or key == "pubsub#" + k) for k in self.args.keys) + + async def start(self): + try: + config_dict = await self.host.bridge.ps_node_configuration_get( + self.args.service, + self.args.node, + self.profile, + ) + except BridgeException as e: + if e.condition == "item-not-found": + self.disp( + f"The node {self.args.node} doesn't exist on {self.args.service}", + error=True, + ) + self.host.quit(C.EXIT_NOT_FOUND) + else: + self.disp(f"can't get node configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + else: + key_filter = (lambda k: True) if not self.args.keys else self.filter_key + config_dict = { + self.remove_prefix(k): v for k, v in config_dict.items() if key_filter(k) + } + await self.output(config_dict) + self.host.quit() + + +class NodeCreate(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "create", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("create a node"), + ) + + @staticmethod + def add_node_config_options(parser): + parser.add_argument( + "-f", + "--field", + action="append", + nargs=2, + dest="fields", + default=[], + metavar=("KEY", "VALUE"), + help=_("configuration field to set"), + ) + parser.add_argument( + "-F", + "--full-prefix", + action="store_true", + help=_('don\'t prepend "pubsub#" prefix to field names'), + ) + + def add_parser_options(self): + self.add_node_config_options(self.parser) + + @staticmethod + def get_config_options(args): + if not args.full_prefix: + return {"pubsub#" + k: v for k, v in args.fields} + else: + return dict(args.fields) + + async def start(self): + options = self.get_config_options(self.args) + try: + node_id = await self.host.bridge.ps_node_create( + self.args.service, + self.args.node, + options, + self.profile, + ) + except Exception as e: + self.disp(msg=_("can't create node: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if self.host.verbosity: + announce = _("node created successfully: ") + else: + announce = "" + self.disp(announce + node_id) + self.host.quit() + + +class NodePurge(base.CommandBase): + def __init__(self, host): + super(NodePurge, self).__init__( + host, + "purge", + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("purge a node (i.e. remove all items from it)"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("purge node without confirmation"), + ) + + async def start(self): + if not self.args.force: + if not self.args.service: + message = _( + "Are you sure to purge PEP node [{node}]? This will " + "delete ALL items from it!" + ).format(node=self.args.node) + else: + message = _( + "Are you sure to delete node [{node}] on service " + "[{service}]? This will delete ALL items from it!" + ).format(node=self.args.node, service=self.args.service) + await self.host.confirm_or_quit(message, _("node purge cancelled")) + + try: + await self.host.bridge.ps_node_purge( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(msg=_("can't purge node: {e}").format(e=e), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("node [{node}] purged successfully").format(node=self.args.node)) + self.host.quit() + + +class NodeDelete(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "delete", + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("delete a node"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--force", + action="store_true", + help=_("delete node without confirmation"), + ) + + async def start(self): + if not self.args.force: + if not self.args.service: + message = _("Are you sure to delete PEP node [{node}] ?").format( + node=self.args.node + ) + else: + message = _( + "Are you sure to delete node [{node}] on " "service [{service}]?" + ).format(node=self.args.node, service=self.args.service) + await self.host.confirm_or_quit(message, _("node deletion cancelled")) + + try: + await self.host.bridge.ps_node_delete( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't delete node: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("node [{node}] deleted successfully").format(node=self.args.node)) + self.host.quit() + + +class NodeSet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("set node configuration"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", + "--field", + action="append", + nargs=2, + dest="fields", + required=True, + metavar=("KEY", "VALUE"), + help=_("configuration field to set (required)"), + ) + self.parser.add_argument( + "-F", + "--full-prefix", + action="store_true", + help=_('don\'t prepend "pubsub#" prefix to field names'), + ) + + def get_key_name(self, k): + if self.args.full_prefix or k.startswith("pubsub#"): + return k + else: + return "pubsub#" + k + + async def start(self): + try: + await self.host.bridge.ps_node_configuration_set( + self.args.service, + self.args.node, + {self.get_key_name(k): v for k, v in self.args.fields}, + self.profile, + ) + except Exception as e: + self.disp(f"can't set node configuration: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("node configuration successful"), 1) + self.host.quit() + + +class NodeImport(base.CommandBase): + def __init__(self, host): + super(NodeImport, self).__init__( + host, + "import", + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("import raw XML to a node"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "--admin", + action="store_true", + help=_("do a pubsub admin request, needed to change publisher"), + ) + self.parser.add_argument( + "import_file", + type=argparse.FileType(), + help=_( + "path to the XML file with data to import. The file must contain " + "whole XML of each item to import." + ), + ) + + async def start(self): + try: + element, etree = xml_tools.etree_parse( + self, self.args.import_file, reraise=True + ) + except Exception as e: + from lxml.etree import XMLSyntaxError + + if isinstance(e, XMLSyntaxError) and e.code == 5: + # we have extra content, this probaby means that item are not wrapped + # so we wrap them here and try again + self.args.import_file.seek(0) + xml_buf = "" + self.args.import_file.read() + "" + element, etree = xml_tools.etree_parse(self, xml_buf) + + # we reverse element as we expect to have most recently published element first + # TODO: make this more explicit and add an option + element[:] = reversed(element) + + if not all([i.tag == "{http://jabber.org/protocol/pubsub}item" for i in element]): + self.disp( + _("You are not using list of pubsub items, we can't import this file"), + error=True, + ) + self.host.quit(C.EXIT_DATA_ERROR) + return + + items = [etree.tostring(i, encoding="unicode") for i in element] + if self.args.admin: + method = self.host.bridge.ps_admin_items_send + else: + self.disp( + _( + "Items are imported without using admin mode, publisher can't " + "be changed" + ) + ) + method = self.host.bridge.ps_items_send + + try: + items_ids = await method( + self.args.service, + self.args.node, + items, + "", + self.profile, + ) + except Exception as e: + self.disp(f"can't send items: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + if items_ids: + self.disp( + _("items published with id(s) {items_ids}").format( + items_ids=", ".join(items_ids) + ) + ) + else: + self.disp(_("items published")) + self.host.quit() + + +class NodeAffiliationsGet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("retrieve node affiliations (for node owner)"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + affiliations = await self.host.bridge.ps_node_affiliations_get( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get node affiliations: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(affiliations) + self.host.quit() + + +class NodeAffiliationsSet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("set affiliations (for node owner)"), + ) + + def add_parser_options(self): + # XXX: we use optional argument syntax for a required one because list of list of 2 elements + # (used to construct dicts) don't work with positional arguments + self.parser.add_argument( + "-a", + "--affiliation", + dest="affiliations", + metavar=("JID", "AFFILIATION"), + required=True, + action="append", + nargs=2, + help=_("entity/affiliation couple(s)"), + ) + + async def start(self): + affiliations = dict(self.args.affiliations) + try: + await self.host.bridge.ps_node_affiliations_set( + self.args.service, + self.args.node, + affiliations, + self.profile, + ) + except Exception as e: + self.disp(f"can't set node affiliations: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("affiliations have been set"), 1) + self.host.quit() + + +class NodeAffiliations(base.CommandBase): + subcommands = (NodeAffiliationsGet, NodeAffiliationsSet) + + def __init__(self, host): + super(NodeAffiliations, self).__init__( + host, + "affiliations", + use_profile=False, + help=_("set or retrieve node affiliations"), + ) + + +class NodeSubscriptionsGet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_DICT, + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("retrieve node subscriptions (for node owner)"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "--public", + action="store_true", + help=_("get public subscriptions"), + ) + + async def start(self): + if self.args.public: + method = self.host.bridge.ps_public_node_subscriptions_get + else: + method = self.host.bridge.ps_node_subscriptions_get + try: + subscriptions = await method( + self.args.service, + self.args.node, + self.profile, + ) + except Exception as e: + self.disp(f"can't get node subscriptions: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + await self.output(subscriptions) + self.host.quit() + + +class StoreSubscriptionAction(argparse.Action): + """Action which handle subscription parameter for owner + + list is given by pairs: jid and subscription state + if subscription state is not specified, it default to "subscribed" + """ + + def __call__(self, parser, namespace, values, option_string): + dest_dict = getattr(namespace, self.dest) + while values: + jid_s = values.pop(0) + try: + subscription = values.pop(0) + except IndexError: + subscription = "subscribed" + if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER: + parser.error( + _("subscription must be one of {}").format( + ", ".join(ALLOWED_SUBSCRIPTIONS_OWNER) + ) + ) + dest_dict[jid_s] = subscription + + +class NodeSubscriptionsSet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("set/modify subscriptions (for node owner)"), + ) + + def add_parser_options(self): + # XXX: we use optional argument syntax for a required one because list of list of 2 elements + # (uses to construct dicts) don't work with positional arguments + self.parser.add_argument( + "-S", + "--subscription", + dest="subscriptions", + default={}, + nargs="+", + metavar=("JID [SUSBSCRIPTION]"), + required=True, + action=StoreSubscriptionAction, + help=_("entity/subscription couple(s)"), + ) + + async def start(self): + try: + self.host.bridge.ps_node_subscriptions_set( + self.args.service, + self.args.node, + self.args.subscriptions, + self.profile, + ) + except Exception as e: + self.disp(f"can't set node subscriptions: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("subscriptions have been set"), 1) + self.host.quit() + + +class NodeSubscriptions(base.CommandBase): + subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet) + + def __init__(self, host): + super(NodeSubscriptions, self).__init__( + host, + "subscriptions", + use_profile=False, + help=_("get or modify node subscriptions"), + ) + + +class NodeSchemaSet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "set", + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("set/replace a schema"), + ) + + def add_parser_options(self): + self.parser.add_argument("schema", help=_("schema to set (must be XML)")) + + async def start(self): + try: + await self.host.bridge.ps_schema_set( + self.args.service, + self.args.node, + self.args.schema, + self.profile, + ) + except Exception as e: + self.disp(f"can't set schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("schema has been set"), 1) + self.host.quit() + + +class NodeSchemaEdit(base.CommandBase, common.BaseEdit): + use_items = False + + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "edit", + use_pubsub=True, + pubsub_flags={C.NODE}, + use_draft=True, + use_verbose=True, + help=_("edit a schema"), + ) + common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR) + + def add_parser_options(self): + pass + + async def publish(self, schema): + try: + await self.host.bridge.ps_schema_set( + self.args.service, + self.args.node, + schema, + self.profile, + ) + except Exception as e: + self.disp(f"can't set schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + else: + self.disp(_("schema has been set"), 1) + self.host.quit() + + async def ps_schema_get_cb(self, schema): + try: + from lxml import etree + except ImportError: + self.disp( + "lxml module must be installed to use edit, please install it " + 'with "pip install lxml"', + error=True, + ) + self.host.quit(1) + content_file_obj, content_file_path = self.get_tmp_file() + schema = schema.strip() + if schema: + parser = etree.XMLParser(remove_blank_text=True) + schema_elt = etree.fromstring(schema, parser) + content_file_obj.write( + etree.tostring(schema_elt, encoding="utf-8", pretty_print=True) + ) + content_file_obj.seek(0) + await self.run_editor( + "pubsub_schema_editor_args", content_file_path, content_file_obj + ) + + async def start(self): + try: + schema = await self.host.bridge.ps_schema_get( + self.args.service, + self.args.node, + self.profile, + ) + except BridgeException as e: + if e.condition == "item-not-found" or e.classname == "NotFound": + schema = "" + else: + self.disp(f"can't edit schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + await self.ps_schema_get_cb(schema) + + +class NodeSchemaGet(base.CommandBase): + def __init__(self, host): + base.CommandBase.__init__( + self, + host, + "get", + use_output=C.OUTPUT_XML, + use_pubsub=True, + pubsub_flags={C.NODE}, + use_verbose=True, + help=_("get schema"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + schema = await self.host.bridge.ps_schema_get( + self.args.service, + self.args.node, + self.profile, + ) + except BridgeException as e: + if e.condition == "item-not-found" or e.classname == "NotFound": + schema = None + else: + self.disp(f"can't get schema: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + if schema: + await self.output(schema) + self.host.quit() + else: + self.disp(_("no schema found"), 1) + self.host.quit(C.EXIT_NOT_FOUND) + + +class NodeSchema(base.CommandBase): + subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet) + + def __init__(self, host): + super(NodeSchema, self).__init__( + host, "schema", use_profile=False, help=_("data schema manipulation") + ) + + +class Node(base.CommandBase): + subcommands = ( + NodeInfo, + NodeCreate, + NodePurge, + NodeDelete, + NodeSet, + NodeImport, + NodeAffiliations, + NodeSubscriptions, + NodeSchema, + ) + + def __init__(self, host): + super(Node, self).__init__( + host, "node", use_profile=False, help=_("node handling") + ) + + +class CacheGet(base.CommandBase): + def __init__(self, host): + super().__init__( + host, + "get", + use_output=C.OUTPUT_LIST_XML, + use_pubsub=True, + pubsub_flags={C.NODE, C.MULTI_ITEMS, C.CACHE}, + help=_("get pubsub item(s) from cache"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-S", + "--sub-id", + default="", + help=_("subscription id"), + ) + + async def start(self): + try: + ps_result = data_format.deserialise( + await self.host.bridge.ps_cache_get( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + self.args.sub_id, + self.get_pubsub_extra(), + self.profile, + ) + ) + except BridgeException as e: + if e.classname == "NotFound": + self.disp( + f"The node {self.args.node} from {self.args.service} is not in cache " + f"for {self.profile}", + error=True, + ) + self.host.quit(C.EXIT_NOT_FOUND) + else: + self.disp(f"can't get pubsub items from cache: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + else: + await self.output(ps_result["items"]) + self.host.quit(C.EXIT_OK) + + +class CacheSync(base.CommandBase): + + def __init__(self, host): + super().__init__( + host, + "sync", + use_pubsub=True, + pubsub_flags={C.NODE}, + help=_("(re)synchronise a pubsub node"), + ) + + def add_parser_options(self): + pass + + async def start(self): + try: + await self.host.bridge.ps_cache_sync( + self.args.service, + self.args.node, + self.profile, + ) + except BridgeException as e: + if e.condition == "item-not-found" or e.classname == "NotFound": + self.disp( + f"The node {self.args.node} doesn't exist on {self.args.service}", + error=True, + ) + self.host.quit(C.EXIT_NOT_FOUND) + else: + self.disp(f"can't synchronise pubsub node: {e}", error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + else: + self.host.quit(C.EXIT_OK) + + +class CachePurge(base.CommandBase): + + def __init__(self, host): + super().__init__( + host, + "purge", + use_profile=False, + help=_("purge (delete) items from cache"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-s", "--service", action="append", metavar="JID", dest="services", + help="purge items only for these services. If not specified, items from ALL " + "services will be purged. May be used several times." + ) + self.parser.add_argument( + "-n", "--node", action="append", dest="nodes", + help="purge items only for these nodes. If not specified, items from ALL " + "nodes will be purged. May be used several times." + ) + self.parser.add_argument( + "-p", "--profile", action="append", dest="profiles", + help="purge items only for these profiles. If not specified, items from ALL " + "profiles will be purged. May be used several times." + ) + self.parser.add_argument( + "-b", "--updated-before", type=base.date_decoder, metavar="TIME_PATTERN", + help="purge items which have been last updated before given time." + ) + self.parser.add_argument( + "-C", "--created-before", type=base.date_decoder, metavar="TIME_PATTERN", + help="purge items which have been last created before given time." + ) + self.parser.add_argument( + "-t", "--type", action="append", dest="types", + help="purge items flagged with TYPE. May be used several times." + ) + self.parser.add_argument( + "-S", "--subtype", action="append", dest="subtypes", + help="purge items flagged with SUBTYPE. May be used several times." + ) + self.parser.add_argument( + "-f", "--force", action="store_true", + help=_("purge items without confirmation") + ) + + async def start(self): + if not self.args.force: + await self.host.confirm_or_quit( + _( + "Are you sure to purge items from cache? You'll have to bypass cache " + "or resynchronise nodes to access deleted items again." + ), + _("Items purgins has been cancelled.") + ) + purge_data = {} + for key in ( + "services", "nodes", "profiles", "updated_before", "created_before", + "types", "subtypes" + ): + value = getattr(self.args, key) + if value is not None: + purge_data[key] = value + try: + await self.host.bridge.ps_cache_purge( + data_format.serialise( + purge_data + ) + ) + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + else: + self.host.quit(C.EXIT_OK) + + +class CacheReset(base.CommandBase): + + def __init__(self, host): + super().__init__( + host, + "reset", + use_profile=False, + help=_("remove everything from cache"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", "--force", action="store_true", + help=_("reset cache without confirmation") + ) + + async def start(self): + if not self.args.force: + await self.host.confirm_or_quit( + _( + "Are you sure to reset cache? All nodes and items will be removed " + "from it, then it will be progressively refilled as if it were new. " + "This may be resources intensive." + ), + _("Pubsub cache reset has been cancelled.") + ) + try: + await self.host.bridge.ps_cache_reset() + except Exception as e: + self.disp(f"Internal error: {e}", error=True) + self.host.quit(C.EXIT_INTERNAL_ERROR) + else: + self.host.quit(C.EXIT_OK) + + +class CacheSearch(base.CommandBase): + def __init__(self, host): + extra_outputs = { + "default": self.default_output, + "xml": self.xml_output, + "xml-raw": self.xml_raw_output, + } + super().__init__( + host, + "search", + use_profile=False, + use_output=C.OUTPUT_LIST_DICT, + extra_outputs=extra_outputs, + help=_("search for pubsub items in cache"), + ) + + def add_parser_options(self): + self.parser.add_argument( + "-f", "--fts", help=_("Full-Text Search query"), metavar="FTS_QUERY" + ) + self.parser.add_argument( + "-p", "--profile", action="append", dest="profiles", metavar="PROFILE", + help="search items only from these profiles. May be used several times." + ) + self.parser.add_argument( + "-s", "--service", action="append", dest="services", metavar="SERVICE", + help="items must be from specified service. May be used several times." + ) + self.parser.add_argument( + "-n", "--node", action="append", dest="nodes", metavar="NODE", + help="items must be in the specified node. May be used several times." + ) + self.parser.add_argument( + "-t", "--type", action="append", dest="types", metavar="TYPE", + help="items must be of specified type. May be used several times." + ) + self.parser.add_argument( + "-S", "--subtype", action="append", dest="subtypes", metavar="SUBTYPE", + help="items must be of specified subtype. May be used several times." + ) + self.parser.add_argument( + "-P", "--payload", action="store_true", help=_("include item XML payload") + ) + self.parser.add_argument( + "-o", "--order-by", action="append", nargs="+", + metavar=("ORDER", "[FIELD] [DIRECTION]"), + help=_("how items must be ordered. May be used several times.") + ) + self.parser.add_argument( + "-l", "--limit", type=int, help=_("maximum number of items to return") + ) + self.parser.add_argument( + "-i", "--index", type=int, help=_("return results starting from this index") + ) + self.parser.add_argument( + "-F", + "--field", + action="append", + nargs=3, + dest="fields", + default=[], + metavar=("PATH", "OPERATOR", "VALUE"), + help=_("parsed data field filter. May be used several times."), + ) + self.parser.add_argument( + "-k", + "--key", + action="append", + dest="keys", + metavar="KEY", + help=_( + "data key(s) to display. May be used several times. DEFAULT: show all " + "keys" + ), + ) + + async def start(self): + query = {} + for arg in ("fts", "profiles", "services", "nodes", "types", "subtypes"): + value = getattr(self.args, arg) + if value: + if arg in ("types", "subtypes"): + # empty string is used to find items without type and/or subtype + value = [v or None for v in value] + query[arg] = value + for arg in ("limit", "index"): + value = getattr(self.args, arg) + if value is not None: + query[arg] = value + if self.args.order_by is not None: + for order_data in self.args.order_by: + order, *args = order_data + if order == "field": + if not args: + self.parser.error(_("field data must be specified in --order-by")) + elif len(args) == 1: + path = args[0] + direction = "asc" + elif len(args) == 2: + path, direction = args + else: + self.parser.error(_( + "You can't specify more that 2 arguments for a field in " + "--order-by" + )) + try: + path = json.loads(path) + except json.JSONDecodeError: + pass + order_query = { + "path": path, + } + else: + order_query = { + "order": order + } + if not args: + direction = "asc" + elif len(args) == 1: + direction = args[0] + else: + self.parser.error(_( + "there are too many arguments in --order-by option" + )) + if direction.lower() not in ("asc", "desc"): + self.parser.error(_("invalid --order-by direction: {direction!r}")) + order_query["direction"] = direction + query.setdefault("order-by", []).append(order_query) + + if self.args.fields: + parsed = [] + for field in self.args.fields: + path, operator, value = field + try: + path = json.loads(path) + except json.JSONDecodeError: + # this is not a JSON encoded value, we keep it as a string + pass + + if not isinstance(path, list): + path = [path] + + # handling of TP(
desc'), + "list": ("\n\n"), + "horizontalrule": ("\n
\n", "", ""), + "image": ('desc'), + }, +} + +# Define here the commands that are supported by the WYSIWYG edition. +# Keys must be the same than the ones used in RICH_SYNTAXES["XHTML"]. +# Values will be used to call execCommand(cmd, False, arg), they can be: +# - a string used for cmd and arg is assumed empty +# - a tuple (cmd, prompt, arg) with cmd the name of the command, +# prompt the text to display for asking a user input and arg is the +# value to use directly without asking the user if prompt is empty. +COMMANDS = { + "bold": "bold", + "italic": "italic", + "underline": "underline", + "code": ("formatBlock", "", "pre"), + "strikethrough": "strikeThrough", + "heading": ("heading", "Please specify the heading level (h1, h2, h3...)", ""), + "link": ("createLink", "Please specify an URL", ""), + "list": "insertUnorderedList", + "horizontalrule": "insertHorizontalRule", + "image": ("insertImage", "Please specify an image path", ""), +} + +# These values should be equal to the ones in plugin_misc_text_syntaxes +# FIXME: should the plugin import them from here to avoid duplicity? Importing +# the plugin's values from here is not possible because Libervia would fail. +PARAM_KEY_COMPOSITION = "Composition" +PARAM_NAME_SYNTAX = "Syntax" diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/css_color.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/css_color.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 + + +# CSS color parsing +# Copyright (C) 2009-2021 Jérome-Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) + + +CSS_COLORS = { + "black": "000000", + "silver": "c0c0c0", + "gray": "808080", + "white": "ffffff", + "maroon": "800000", + "red": "ff0000", + "purple": "800080", + "fuchsia": "ff00ff", + "green": "008000", + "lime": "00ff00", + "olive": "808000", + "yellow": "ffff00", + "navy": "000080", + "blue": "0000ff", + "teal": "008080", + "aqua": "00ffff", + "orange": "ffa500", + "aliceblue": "f0f8ff", + "antiquewhite": "faebd7", + "aquamarine": "7fffd4", + "azure": "f0ffff", + "beige": "f5f5dc", + "bisque": "ffe4c4", + "blanchedalmond": "ffebcd", + "blueviolet": "8a2be2", + "brown": "a52a2a", + "burlywood": "deb887", + "cadetblue": "5f9ea0", + "chartreuse": "7fff00", + "chocolate": "d2691e", + "coral": "ff7f50", + "cornflowerblue": "6495ed", + "cornsilk": "fff8dc", + "crimson": "dc143c", + "darkblue": "00008b", + "darkcyan": "008b8b", + "darkgoldenrod": "b8860b", + "darkgray": "a9a9a9", + "darkgreen": "006400", + "darkgrey": "a9a9a9", + "darkkhaki": "bdb76b", + "darkmagenta": "8b008b", + "darkolivegreen": "556b2f", + "darkorange": "ff8c00", + "darkorchid": "9932cc", + "darkred": "8b0000", + "darksalmon": "e9967a", + "darkseagreen": "8fbc8f", + "darkslateblue": "483d8b", + "darkslategray": "2f4f4f", + "darkslategrey": "2f4f4f", + "darkturquoise": "00ced1", + "darkviolet": "9400d3", + "deeppink": "ff1493", + "deepskyblue": "00bfff", + "dimgray": "696969", + "dimgrey": "696969", + "dodgerblue": "1e90ff", + "firebrick": "b22222", + "floralwhite": "fffaf0", + "forestgreen": "228b22", + "gainsboro": "dcdcdc", + "ghostwhite": "f8f8ff", + "gold": "ffd700", + "goldenrod": "daa520", + "greenyellow": "adff2f", + "grey": "808080", + "honeydew": "f0fff0", + "hotpink": "ff69b4", + "indianred": "cd5c5c", + "indigo": "4b0082", + "ivory": "fffff0", + "khaki": "f0e68c", + "lavender": "e6e6fa", + "lavenderblush": "fff0f5", + "lawngreen": "7cfc00", + "lemonchiffon": "fffacd", + "lightblue": "add8e6", + "lightcoral": "f08080", + "lightcyan": "e0ffff", + "lightgoldenrodyellow": "fafad2", + "lightgray": "d3d3d3", + "lightgreen": "90ee90", + "lightgrey": "d3d3d3", + "lightpink": "ffb6c1", + "lightsalmon": "ffa07a", + "lightseagreen": "20b2aa", + "lightskyblue": "87cefa", + "lightslategray": "778899", + "lightslategrey": "778899", + "lightsteelblue": "b0c4de", + "lightyellow": "ffffe0", + "limegreen": "32cd32", + "linen": "faf0e6", + "mediumaquamarine": "66cdaa", + "mediumblue": "0000cd", + "mediumorchid": "ba55d3", + "mediumpurple": "9370db", + "mediumseagreen": "3cb371", + "mediumslateblue": "7b68ee", + "mediumspringgreen": "00fa9a", + "mediumturquoise": "48d1cc", + "mediumvioletred": "c71585", + "midnightblue": "191970", + "mintcream": "f5fffa", + "mistyrose": "ffe4e1", + "moccasin": "ffe4b5", + "navajowhite": "ffdead", + "oldlace": "fdf5e6", + "olivedrab": "6b8e23", + "orangered": "ff4500", + "orchid": "da70d6", + "palegoldenrod": "eee8aa", + "palegreen": "98fb98", + "paleturquoise": "afeeee", + "palevioletred": "db7093", + "papayawhip": "ffefd5", + "peachpuff": "ffdab9", + "peru": "cd853f", + "pink": "ffc0cb", + "plum": "dda0dd", + "powderblue": "b0e0e6", + "rosybrown": "bc8f8f", + "royalblue": "4169e1", + "saddlebrown": "8b4513", + "salmon": "fa8072", + "sandybrown": "f4a460", + "seagreen": "2e8b57", + "seashell": "fff5ee", + "sienna": "a0522d", + "skyblue": "87ceeb", + "slateblue": "6a5acd", + "slategray": "708090", + "slategrey": "708090", + "snow": "fffafa", + "springgreen": "00ff7f", + "steelblue": "4682b4", + "tan": "d2b48c", + "thistle": "d8bfd8", + "tomato": "ff6347", + "turquoise": "40e0d0", + "violet": "ee82ee", + "wheat": "f5deb3", + "whitesmoke": "f5f5f5", + "yellowgreen": "9acd32", + "rebeccapurple": "663399", +} +DEFAULT = "000000" + + +def parse(raw_value, as_string=True): + """parse CSS color value and return normalised value + + @param raw_value(unicode): CSS value + @param as_string(bool): if True return a string, + else return a tuple of int + @return (unicode, tuple): normalised value + if as_string is True, value is 3 or 4 hex words (e.g. u"ff00aabb") + else value is a 3 or 4 tuple of int (e.g.: (255, 0, 170, 187)). + If present, the 4th value is the alpha channel + If value can't be parsed, a warning message is logged, and DEFAULT is returned + """ + raw_value = raw_value.strip().lower() + if raw_value.startswith("#"): + # we have a hexadecimal value + str_value = raw_value[1:] + if len(raw_value) in (3, 4): + str_value = "".join([2 * v for v in str_value]) + elif raw_value.startswith("rgb"): + left_p = raw_value.find("(") + right_p = raw_value.find(")") + rgb_values = [v.strip() for v in raw_value[left_p + 1 : right_p].split(",")] + expected_len = 4 if raw_value.startswith("rgba") else 3 + if len(rgb_values) != expected_len: + log.warning("incorrect value: {}".format(raw_value)) + str_value = DEFAULT + else: + int_values = [] + for rgb_v in rgb_values: + p_idx = rgb_v.find("%") + if p_idx == -1: + # base 10 value + try: + int_v = int(rgb_v) + if int_v > 255: + raise ValueError("value exceed 255") + int_values.append(int_v) + except ValueError: + log.warning("invalid int: {}".format(rgb_v)) + int_values.append(0) + else: + # percentage + try: + int_v = int(int(rgb_v[:p_idx]) / 100.0 * 255) + if int_v > 255: + raise ValueError("value exceed 255") + int_values.append(int_v) + except ValueError: + log.warning("invalid percent value: {}".format(rgb_v)) + int_values.append(0) + str_value = "".join(["{:02x}".format(v) for v in int_values]) + elif raw_value.startswith("hsl"): + log.warning("hue-saturation-lightness not handled yet") # TODO + str_value = DEFAULT + else: + try: + str_value = CSS_COLORS[raw_value] + except KeyError: + log.warning("unrecognised format: {}".format(raw_value)) + str_value = DEFAULT + + if as_string: + return str_value + else: + return tuple( + [ + int(str_value[i] + str_value[i + 1], 16) + for i in range(0, len(str_value), 2) + ] + ) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/games.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/games.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This library help manage general games (e.g. card games) and it is shared by the frontends""" + +SUITS_ORDER = [ + "pique", + "coeur", + "trefle", + "carreau", + "atout", +] # I have switched the usual order 'trefle' and 'carreau' because card are more easy to see if suit colour change (black, red, black, red) +VALUES_ORDER = [str(i) for i in range(1, 11)] + ["valet", "cavalier", "dame", "roi"] + + +class TarotCard(object): + """This class is used to represent a car logically""" + + def __init__(self, tuple_card): + """@param tuple_card: tuple (suit, value)""" + self.suit, self.value = tuple_card + self.bout = self.suit == "atout" and self.value in ["1", "21", "excuse"] + if self.bout or self.value == "roi": + self.points = 4.5 + elif self.value == "dame": + self.points = 3.5 + elif self.value == "cavalier": + self.points = 2.5 + elif self.value == "valet": + self.points = 1.5 + else: + self.points = 0.5 + + def get_tuple(self): + return (self.suit, self.value) + + @staticmethod + def from_tuples(tuple_list): + result = [] + for card_tuple in tuple_list: + result.append(TarotCard(card_tuple)) + return result + + def __cmp__(self, other): + if other is None: + return 1 + if self.suit != other.suit: + idx1 = SUITS_ORDER.index(self.suit) + idx2 = SUITS_ORDER.index(other.suit) + return idx1.__cmp__(idx2) + if self.suit == "atout": + if self.value == other.value == "excuse": + return 0 + if self.value == "excuse": + return -1 + if other.value == "excuse": + return 1 + return int(self.value).__cmp__(int(other.value)) + # at this point we have the same suit which is not 'atout' + idx1 = VALUES_ORDER.index(self.value) + idx2 = VALUES_ORDER.index(other.value) + return idx1.__cmp__(idx2) + + def __str__(self): + return "[%s,%s]" % (self.suit, self.value) + + +# These symbols are diplayed by Libervia next to the player's nicknames +SYMBOLS = {"Radiocol": ["♬"], "Tarot": ["♠", "♣", "♥", "♦"]} diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/host_listener.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/host_listener.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""This module is only used launch callbacks when host is ready, used for early initialisation stuffs""" + + +listeners = [] + + +def addListener(cb): + """Add a listener which will be called when host is ready + + @param cb: callback which will be called when host is ready with host as only argument + """ + listeners.append(cb) + + +def call_listeners(host): + """Must be called by frontend when host is ready. + + The call will launch all the callbacks, then remove the listeners list. + @param host(QuickApp): the instancied QuickApp subclass + """ + global listeners + while True: + try: + cb = listeners.pop(0) + cb(host) + except IndexError: + break + del listeners diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/jid.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/jid.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +# Libervia XMPP +# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Optional, Tuple + + +class JID(str): + """This class helps manage JID (@/)""" + + def __new__(cls, jid_str: str) -> "JID": + return str.__new__(cls, cls._normalize(jid_str)) + + def __init__(self, jid_str: str): + self._node, self._domain, self._resource = self._parse() + + @staticmethod + def _normalize(jid_str: str) -> str: + """Naive normalization before instantiating and parsing the JID""" + if not jid_str: + return jid_str + tokens = jid_str.split("/") + tokens[0] = tokens[0].lower() # force node and domain to lower-case + return "/".join(tokens) + + def _parse(self) -> Tuple[Optional[str], str, Optional[str]]: + """Find node, domain, and resource from JID""" + node_end = self.find("@") + if node_end < 0: + node_end = 0 + domain_end = self.find("/") + if domain_end == 0: + raise ValueError("a jid can't start with '/'") + if domain_end == -1: + domain_end = len(self) + node = self[:node_end] or None + domain = self[(node_end + 1) if node_end else 0 : domain_end] + resource = self[domain_end + 1 :] or None + return node, domain, resource + + @property + def node(self) -> Optional[str]: + return self._node + + @property + def local(self) -> Optional[str]: + return self._node + + @property + def domain(self) -> str: + return self._domain + + @property + def resource(self) -> Optional[str]: + return self._resource + + @property + def bare(self) -> "JID": + if not self.node: + return JID(self.domain) + return JID(f"{self.node}@{self.domain}") + + def change_resource(self, resource: str) -> "JID": + """Build a new JID with the same node and domain but a different resource. + + @param resource: The new resource for the JID. + @return: A new JID instance with the updated resource. + """ + return JID(f"{self.bare}/{resource}") + + def is_valid(self) -> bool: + """ + @return: True if the JID is XMPP compliant + """ + # Simple check for domain part + if not self.domain or self.domain.startswith(".") or self.domain.endswith("."): + return False + if ".." in self.domain: + return False + return True + + +def new_resource(entity: JID, resource: str) -> JID: + """Build a new JID from the given entity and resource. + + @param entity: original JID + @param resource: new resource + @return: a new JID instance + """ + return entity.change_resource(resource) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/misc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/misc.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + + +# SAT helpers methods for plugins +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class InputHistory(object): + def _update_input_history(self, text=None, step=None, callback=None, mode=""): + """Update the lists of previously sent messages. Several lists can be + handled as they are stored in a dictionary, the argument "mode" being + used as the entry key. There's also a temporary list to allow you play + with previous entries before sending a new message. Parameters values + can be combined: text is None and step is None to initialize a main + list and the temporary one, step is None to update a list and + reinitialize the temporary one, step is not None to update + the temporary list between two messages. + @param text: text to be saved. + @param step: step to move the temporary index. + @param callback: method to display temporary entries. + @param mode: the dictionary key for main lists. + """ + if not hasattr(self, "input_histories"): + self.input_histories = {} + history = self.input_histories.setdefault(mode, []) + if step is None and text is not None: + # update the main list + if text in history: + history.remove(text) + history.append(text) + length = len(history) + if step is None or length == 0: + # prepare the temporary list and index + self.input_history_tmp = history[:] + self.input_history_tmp.append("") + self.input_history_index = length + if step is None: + return + # update the temporary list + if text is not None: + # save the current entry + self.input_history_tmp[self.input_history_index] = text + # move to another entry if possible + index_tmp = self.input_history_index + step + if index_tmp >= 0 and index_tmp < len(self.input_history_tmp): + if callback is not None: + callback(self.input_history_tmp[index_tmp]) + self.input_history_index = index_tmp + + +class FlagsHandler(object): + """Small class to handle easily option flags + + the instance is initialized with an iterable + then attribute return True if flag is set, False else. + """ + + def __init__(self, flags): + self.flags = set(flags or []) + self._used_flags = set() + + def __getattr__(self, flag): + self._used_flags.add(flag) + return flag in self.flags + + def __getitem__(self, flag): + return getattr(self, flag) + + def __len__(self): + return len(self.flags) + + def __iter__(self): + return self.flags.__iter__() + + @property + def all_used(self): + """Return True if all flags have been used""" + return self._used_flags.issuperset(self.flags) + + @property + def unused(self): + """Return flags which has not been used yet""" + return self.flags.difference(self._used_flags) diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/strings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/strings.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + + +# SAT helpers methods for plugins +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import re + +# Regexp from http://daringfireball.net/2010/07/improved_regex_for_matching_urls +RE_URL = re.compile( + r"""(?i)\b((?:[a-z]{3,}://|(www|ftp)\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/|mailto:|xmpp:|geo:)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?]))""" +) + + +# TODO: merge this class with an other module or at least rename it (strings is not a good name) + + +def get_url_params(url): + """This comes from pyjamas.Location.makeUrlDict with a small change + to also parse full URLs, and parameters with no value specified + (in that case the default value "" is used). + @param url: any URL with or without parameters + @return: a dictionary of the parameters, if any was given, or {} + """ + dict_ = {} + if "/" in url: + # keep the part after the last "/" + url = url[url.rindex("/") + 1 :] + if url.startswith("?"): + # remove the first "?" + url = url[1:] + pairs = url.split("&") + for pair in pairs: + if len(pair) < 3: + continue + kv = pair.split("=", 1) + dict_[kv[0]] = kv[1] if len(kv) > 1 else "" + return dict_ + + +def add_url_to_text(string, new_target=True): + """Check a text for what looks like an URL and make it clickable. + + @param string (unicode): text to process + @param new_target (bool): if True, make the link open in a new window + """ + # XXX: report any change to libervia.browser.strings.add_url_to_text + def repl(match): + url = match.group(0) + if not re.match(r"""[a-z]{3,}://|mailto:|xmpp:""", url): + url = "http://" + url + target = ' target="_blank"' if new_target else "" + return '%s' % (url, target, match.group(0)) + + return RE_URL.sub(repl, string) + + +def add_url_to_image(string): + """Check a XHTML text for what looks like an imageURL and make it clickable. + + @param string (unicode): text to process + """ + # XXX: report any change to libervia.browser.strings.add_url_to_image + def repl(match): + url = match.group(1) + return '%s' % (url, match.group(0)) + + pattern = r"""]* src="([^"]+)"[^>]*>""" + return re.sub(pattern, repl, string) + + +def fix_xhtml_links(xhtml): + """Add http:// if the scheme is missing and force opening in a new window. + + @param string (unicode): XHTML Content + """ + subs = [] + for match in re.finditer(r'', xhtml): + tag = match.group(0) + url = re.search(r'href="([^"]*)"', tag) + if url and not url.group(1).startswith("#"): # skip internal anchor + if not re.search(r'target="([^"]*)"', tag): # no target + subs.append((tag, '. + +"""This library help manage XML used in SàT frontends """ + +# we don't import minidom as a different class can be used in frontends +# (e.g. NativeDOM in Libervia) + + +def inline_root(doc): + """ make the root attribute inline + @param root_node: minidom's Document compatible class + @return: plain XML + """ + root_elt = doc.documentElement + if root_elt.hasAttribute("style"): + styles_raw = root_elt.getAttribute("style") + styles = styles_raw.split(";") + new_styles = [] + for style in styles: + try: + key, value = style.split(":") + except ValueError: + continue + if key.strip().lower() == "display": + value = "inline" + new_styles.append("%s: %s" % (key.strip(), value.strip())) + root_elt.setAttribute("style", "; ".join(new_styles)) + else: + root_elt.setAttribute("style", "display: inline") + return root_elt.toxml() diff -r 7c5654c54fed -r 26b7ed2817da libervia/frontends/tools/xmlui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/xmlui.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1149 @@ +#!/usr/bin/env python3 + + +# SàT frontend tools +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from libervia.frontends.quick_frontend.constants import Const as C +from libervia.backend.core import exceptions + + +_class_map = {} +CLASS_PANEL = "panel" +CLASS_DIALOG = "dialog" +CURRENT_LABEL = "current_label" +HIDDEN = "hidden" + + +class InvalidXMLUI(Exception): + pass + + +class ClassNotRegistedError(Exception): + pass + + +# FIXME: this method is duplicated in frontends.tools.xmlui.get_text +def get_text(node): + """Get child text nodes + @param node: dom Node + @return: joined unicode text of all nodes + + """ + data = [] + for child in node.childNodes: + if child.nodeType == child.TEXT_NODE: + data.append(child.wholeText) + return "".join(data) + + +class Widget(object): + """base Widget""" + + pass + + +class EmptyWidget(Widget): + """Just a placeholder widget""" + + pass + + +class TextWidget(Widget): + """Non interactive text""" + + pass + + +class LabelWidget(Widget): + """Non interactive text""" + + pass + + +class JidWidget(Widget): + """Jabber ID""" + + pass + + +class DividerWidget(Widget): + """Separator""" + + pass + + +class StringWidget(Widget): + """Input widget wich require a string + + often called Edit in toolkits + """ + + pass + + +class JidInputWidget(Widget): + """Input widget wich require a string + + often called Edit in toolkits + """ + + pass + + +class PasswordWidget(Widget): + """Input widget with require a masked string""" + + pass + + +class TextBoxWidget(Widget): + """Input widget with require a long, possibly multilines string + + often called TextArea in toolkits + """ + + pass + + +class XHTMLBoxWidget(Widget): + """Input widget specialised in XHTML editing, + + a WYSIWYG or specialised editor is expected + """ + + pass + + +class BoolWidget(Widget): + """Input widget with require a boolean value + often called CheckBox in toolkits + """ + + pass + + +class IntWidget(Widget): + """Input widget with require an integer""" + + pass + + +class ButtonWidget(Widget): + """A clickable widget""" + + pass + + +class ListWidget(Widget): + """A widget able to show/choose one or several strings in a list""" + + pass + + +class JidsListWidget(Widget): + """A widget able to show/choose one or several strings in a list""" + + pass + + +class Container(Widget): + """Widget which can contain other ones with a specific layout""" + + @classmethod + def _xmlui_adapt(cls, instance): + """Make cls as instance.__class__ + + cls must inherit from original instance class + Usefull when you get a class from UI toolkit + """ + assert instance.__class__ in cls.__bases__ + instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) + + +class PairsContainer(Container): + """Widgets are disposed in rows of two (usually label/input)""" + + pass + + +class LabelContainer(Container): + """Widgets are associated with label or empty widget""" + + pass + + +class TabsContainer(Container): + """A container which several other containers in tabs + + Often called Notebook in toolkits + """ + + pass + + +class VerticalContainer(Container): + """Widgets are disposed vertically""" + + pass + + +class AdvancedListContainer(Container): + """Widgets are disposed in rows with advaned features""" + + pass + + +class Dialog(object): + """base dialog""" + + def __init__(self, _xmlui_parent): + self._xmlui_parent = _xmlui_parent + + def _xmlui_validated(self, data=None): + if data is None: + data = {} + self._xmlui_set_data(C.XMLUI_STATUS_VALIDATED, data) + self._xmlui_submit(data) + + def _xmlui_cancelled(self): + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + self._xmlui_set_data(C.XMLUI_STATUS_CANCELLED, data) + self._xmlui_submit(data) + + def _xmlui_submit(self, data): + if self._xmlui_parent.submit_id is None: + log.debug(_("Nothing to submit")) + else: + self._xmlui_parent.submit(data) + + def _xmlui_set_data(self, status, data): + pass + + +class MessageDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + pass + + +class NoteDialog(Dialog): + """Short message which doesn't need user confirmation to disappear""" + + pass + + +class ConfirmDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + def _xmlui_set_data(self, status, data): + if status == C.XMLUI_STATUS_VALIDATED: + data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE + elif status == C.XMLUI_STATUS_CANCELLED: + data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE + + +class FileDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + pass + + +class XMLUIBase(object): + """Base class to construct SàT XML User Interface + + This class must not be instancied directly + """ + + def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, + profile=C.PROF_KEY_NONE): + """Initialise the XMLUI instance + + @param host: %(doc_host)s + @param parsed_dom: main parsed dom + @param title: force the title, or use XMLUI one if None + @param flags: list of string which can be: + - NO_CANCEL: the UI can't be cancelled + - FROM_BACKEND: the UI come from backend (i.e. it's not the direct result of + user operation) + @param callback(callable, None): if not None, will be used with action_launch: + - if None is used, default behaviour will be used (closing the dialog and + calling host.action_manager) + - if a callback is provided, it will be used instead, so you'll have to manage + dialog closing or new xmlui to display, or other action (you can call + host.action_manager) + The callback will have data, callback_id and profile as arguments + """ + self.host = host + top = parsed_dom.documentElement + self.session_id = top.getAttribute("session_id") or None + self.submit_id = top.getAttribute("submit") or None + self.xmlui_title = title or top.getAttribute("title") or "" + self.hidden = {} + if flags is None: + flags = [] + self.flags = flags + self.callback = callback or self._default_cb + self.profile = profile + + @property + def user_action(self): + return "FROM_BACKEND" not in self.flags + + def _default_cb(self, data, cb_id, profile): + # TODO: when XMLUI updates will be managed, the _xmlui_close + # must be called only if there is no update + self._xmlui_close() + self.host.action_manager(data, profile=profile) + + def _is_attr_set(self, name, node): + """Return widget boolean attribute status + + @param name: name of the attribute (e.g. "read_only") + @param node: Node instance + @return (bool): True if widget's attribute is set (C.BOOL_TRUE) + """ + read_only = node.getAttribute(name) or C.BOOL_FALSE + return read_only.lower().strip() == C.BOOL_TRUE + + def _get_child_node(self, node, name): + """Return the first child node with the given name + + @param node: Node instance + @param name: name of the wanted node + + @return: The found element or None + """ + for child in node.childNodes: + if child.nodeName == name: + return child + return None + + def submit(self, data): + self._xmlui_close() + if self.submit_id is None: + raise ValueError("Can't submit is self.submit_id is not set") + if "session_id" in data: + raise ValueError( + "session_id must no be used in data, it is automaticaly filled with " + "self.session_id if present" + ) + if self.session_id is not None: + data["session_id"] = self.session_id + self._xmlui_launch_action(self.submit_id, data) + + def _xmlui_launch_action(self, action_id, data): + self.host.action_launch( + action_id, data, callback=self.callback, profile=self.profile + ) + + def _xmlui_close(self): + """Close the window/popup/... where the constructor XMLUI is + + this method must be overrided + """ + raise NotImplementedError + + +class ValueGetter(object): + """dict like object which return values of widgets""" + # FIXME: widget which can keep multiple values are not handled + + def __init__(self, widgets, attr="value"): + self.attr = attr + self.widgets = widgets + + def __getitem__(self, name): + return getattr(self.widgets[name], self.attr) + + def __getattr__(self, name): + return self.__getitem__(name) + + def keys(self): + return list(self.widgets.keys()) + + def items(self): + for name, widget in self.widgets.items(): + try: + value = widget.value + except AttributeError: + try: + value = list(widget.values) + except AttributeError: + continue + yield name, value + + +class XMLUIPanel(XMLUIBase): + """XMLUI Panel + + New frontends can inherit this class to easily implement XMLUI + @property widget_factory: factory to create frontend-specific widgets + @property dialog_factory: factory to create frontend-specific dialogs + """ + + widget_factory = None + + def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, + ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): + """ + + @param title(unicode, None): title of the + @property widgets(dict): widget name => widget map + @property widget_value(ValueGetter): retrieve widget value from it's name + """ + super(XMLUIPanel, self).__init__( + host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile + ) + self.ctrl_list = {} # input widget, used mainly for forms + self.widgets = {} #  allow to access any named widgets + self.widget_value = ValueGetter(self.widgets) + self._main_cont = None + if ignore is None: + ignore = [] + self._ignore = ignore + if whitelist is not None: + if ignore: + raise exceptions.InternalError( + "ignore and whitelist must not be used at the same time" + ) + self._whitelist = whitelist + else: + self._whitelist = None + self.construct_ui(parsed_dom) + + @staticmethod + def escape(name): + """Return escaped name for forms""" + return "%s%s" % (C.SAT_FORM_PREFIX, name) + + @property + def main_cont(self): + return self._main_cont + + @property + def values(self): + """Dict of all widgets values""" + return dict(self.widget_value.items()) + + @main_cont.setter + def main_cont(self, value): + if self._main_cont is not None: + raise ValueError(_("XMLUI can have only one main container")) + self._main_cont = value + + def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None): + """Recursively parse childNodes of an element + + @param _xmlui_parent: widget container with '_xmlui_append' method + @param current_node: element from which childs will be parsed + @param wanted: list of tag names that can be present in the childs to be SàT XMLUI + compliant + @param data(None, dict): additionnal data which are needed in some cases + """ + for node in current_node.childNodes: + if data is None: + data = {} + if wanted and not node.nodeName in wanted: + raise InvalidXMLUI("Unexpected node: [%s]" % node.nodeName) + + if node.nodeName == "container": + type_ = node.getAttribute("type") + if _xmlui_parent is self and type_ not in ("vertical", "tabs"): + # main container is not a VerticalContainer and we want one, + # so we create one to wrap it + _xmlui_parent = self.widget_factory.createVerticalContainer(self) + self.main_cont = _xmlui_parent + if type_ == "tabs": + cont = self.widget_factory.createTabsContainer(_xmlui_parent) + self._parse_childs(_xmlui_parent, node, ("tab",), {"tabs_cont": cont}) + elif type_ == "vertical": + cont = self.widget_factory.createVerticalContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + elif type_ == "pairs": + cont = self.widget_factory.createPairsContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + elif type_ == "label": + cont = self.widget_factory.createLabelContainer(_xmlui_parent) + self._parse_childs( + # FIXME: the "None" value for CURRENT_LABEL doesn't seem + # used or even useful, it should probably be removed + # and all "is not None" tests for it should be removed too + # to be checked for 0.8 + cont, node, ("widget", "container"), {CURRENT_LABEL: None} + ) + elif type_ == "advanced_list": + try: + columns = int(node.getAttribute("columns")) + except (TypeError, ValueError): + raise exceptions.DataError("Invalid columns") + selectable = node.getAttribute("selectable") or "no" + auto_index = node.getAttribute("auto_index") == C.BOOL_TRUE + data = {"index": 0} if auto_index else None + cont = self.widget_factory.createAdvancedListContainer( + _xmlui_parent, columns, selectable + ) + callback_id = node.getAttribute("callback") or None + if callback_id is not None: + if selectable == "no": + raise ValueError( + "can't have selectable=='no' and callback_id at the same time" + ) + cont._xmlui_callback_id = callback_id + cont._xmlui_on_select(self.on_adv_list_select) + + self._parse_childs(cont, node, ("row",), data) + else: + log.warning(_("Unknown container [%s], using default one") % type_) + cont = self.widget_factory.createVerticalContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + try: + xmluiAppend = _xmlui_parent._xmlui_append + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + if _xmlui_parent is self: + self.main_cont = cont + else: + raise Exception( + _("Internal Error, container has not _xmlui_append method") + ) + else: + xmluiAppend(cont) + + elif node.nodeName == "tab": + name = node.getAttribute("name") + label = node.getAttribute("label") + selected = C.bool(node.getAttribute("selected") or C.BOOL_FALSE) + if not name or not "tabs_cont" in data: + raise InvalidXMLUI + if self.type == "param": + self._current_category = ( + name + ) # XXX: awful hack because params need category and we don't keep parent + tab_cont = data["tabs_cont"] + new_tab = tab_cont._xmlui_add_tab(label or name, selected) + self._parse_childs(new_tab, node, ("widget", "container")) + + elif node.nodeName == "row": + try: + index = str(data["index"]) + except KeyError: + index = node.getAttribute("index") or None + else: + data["index"] += 1 + _xmlui_parent._xmlui_add_row(index) + self._parse_childs(_xmlui_parent, node, ("widget", "container")) + + elif node.nodeName == "widget": + name = node.getAttribute("name") + if name and ( + name in self._ignore + or self._whitelist is not None + and name not in self._whitelist + ): + # current widget is ignored, but there may be already a label + if CURRENT_LABEL in data: + curr_label = data.pop(CURRENT_LABEL) + if curr_label is not None: + # if so, we remove it from parent + _xmlui_parent._xmlui_remove(curr_label) + continue + type_ = node.getAttribute("type") + value_elt = self._get_child_node(node, "value") + if value_elt is not None: + value = get_text(value_elt) + else: + value = ( + node.getAttribute("value") if node.hasAttribute("value") else "" + ) + if type_ == "empty": + ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent) + if CURRENT_LABEL in data: + data[CURRENT_LABEL] = None + elif type_ == "text": + ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value) + elif type_ == "label": + ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value) + data[CURRENT_LABEL] = ctrl + elif type_ == "hidden": + if name in self.hidden: + raise exceptions.ConflictError("Conflict on hidden value with " + "name {name}".format(name=name)) + self.hidden[name] = value + continue + elif type_ == "jid": + ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value) + elif type_ == "divider": + style = node.getAttribute("style") or "line" + ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style) + elif type_ == "string": + ctrl = self.widget_factory.createStringWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "jid_input": + ctrl = self.widget_factory.createJidInputWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "password": + ctrl = self.widget_factory.createPasswordWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "textbox": + ctrl = self.widget_factory.createTextBoxWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "xhtmlbox": + ctrl = self.widget_factory.createXHTMLBoxWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "bool": + ctrl = self.widget_factory.createBoolWidget( + _xmlui_parent, + value == C.BOOL_TRUE, + self._is_attr_set("read_only", node), + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "int": + ctrl = self.widget_factory.createIntWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "list": + style = [] if node.getAttribute("multi") == "yes" else ["single"] + for attr in ("noselect", "extensible", "reducible", "inline"): + if node.getAttribute(attr) == "yes": + style.append(attr) + _options = [ + (option.getAttribute("value"), option.getAttribute("label")) + for option in node.getElementsByTagName("option") + ] + _selected = [ + option.getAttribute("value") + for option in node.getElementsByTagName("option") + if option.getAttribute("selected") == C.BOOL_TRUE + ] + ctrl = self.widget_factory.createListWidget( + _xmlui_parent, _options, _selected, style + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "jids_list": + style = [] + jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")] + ctrl = self.widget_factory.createJidsListWidget( + _xmlui_parent, jids, style + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "button": + callback_id = node.getAttribute("callback") + ctrl = self.widget_factory.createButtonWidget( + _xmlui_parent, value, self.on_button_press + ) + ctrl._xmlui_param_id = ( + callback_id, + [ + field.getAttribute("name") + for field in node.getElementsByTagName("field_back") + ], + ) + else: + log.error( + _("FIXME FIXME FIXME: widget type [%s] is not implemented") + % type_ + ) + raise NotImplementedError( + _("FIXME FIXME FIXME: type [%s] is not implemented") % type_ + ) + + if name: + self.widgets[name] = ctrl + + if self.type == "param" and type_ not in ("text", "button"): + try: + ctrl._xmlui_on_change(self.on_param_change) + ctrl._param_category = self._current_category + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead + # of an AttributeError + if not isinstance( + ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget) + ): + log.warning(_("No change listener on [%s]") % ctrl) + + elif type_ != "text": + callback = node.getAttribute("internal_callback") or None + if callback: + fields = [ + field.getAttribute("name") + for field in node.getElementsByTagName("internal_field") + ] + cb_data = self.get_internal_callback_data(callback, node) + ctrl._xmlui_param_internal = (callback, fields, cb_data) + if type_ == "button": + ctrl._xmlui_on_click(self.on_change_internal) + else: + ctrl._xmlui_on_change(self.on_change_internal) + + ctrl._xmlui_name = name + _xmlui_parent._xmlui_append(ctrl) + if CURRENT_LABEL in data and not isinstance(ctrl, LabelWidget): + curr_label = data.pop(CURRENT_LABEL) + if curr_label is not None: + # this key is set in LabelContainer, when present + # we can associate the label with the widget it is labelling + curr_label._xmlui_for_name = name + + else: + raise NotImplementedError(_("Unknown tag [%s]") % node.nodeName) + + def construct_ui(self, parsed_dom, post_treat=None): + """Actually construct the UI + + @param parsed_dom: main parsed dom + @param post_treat: frontend specific treatments to do once the UI is constructed + @return: constructed widget + """ + top = parsed_dom.documentElement + self.type = top.getAttribute("type") + if top.nodeName != "sat_xmlui" or not self.type in [ + "form", + "param", + "window", + "popup", + ]: + raise InvalidXMLUI + + if self.type == "param": + self.param_changed = set() + + self._parse_childs(self, parsed_dom.documentElement) + + if post_treat is not None: + post_treat() + + def _xmlui_set_param(self, name, value, category): + self.host.bridge.param_set(name, value, category, profile_key=self.profile) + + ##EVENTS## + + def on_param_change(self, ctrl): + """Called when type is param and a widget to save is modified + + @param ctrl: widget modified + """ + assert self.type == "param" + self.param_changed.add(ctrl) + + def on_adv_list_select(self, ctrl): + data = {} + widgets = ctrl._xmlui_get_selected_widgets() + for wid in widgets: + try: + name = self.escape(wid._xmlui_name) + value = wid._xmlui_get_value() + data[name] = value + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + pass + idx = ctrl._xmlui_get_selected_index() + if idx is not None: + data["index"] = idx + callback_id = ctrl._xmlui_callback_id + if callback_id is None: + log.info(_("No callback_id found")) + return + self._xmlui_launch_action(callback_id, data) + + def on_button_press(self, button): + """Called when an XMLUI button is clicked + + Launch the action associated to the button + @param button: the button clicked + """ + callback_id, fields = button._xmlui_param_id + if not callback_id: # the button is probably bound to an internal action + return + data = {} + for field in fields: + escaped = self.escape(field) + ctrl = self.ctrl_list[field] + if isinstance(ctrl["control"], ListWidget): + data[escaped] = "\t".join(ctrl["control"]._xmlui_get_selected_values()) + else: + data[escaped] = ctrl["control"]._xmlui_get_value() + self._xmlui_launch_action(callback_id, data) + + def on_change_internal(self, ctrl): + """Called when a widget that has been bound to an internal callback is changed. + + This is used to perform some UI actions without communicating with the backend. + See sat.tools.xml_tools.Widget.set_internal_callback for more details. + @param ctrl: widget modified + """ + action, fields, data = ctrl._xmlui_param_internal + if action not in ("copy", "move", "groups_of_contact"): + raise NotImplementedError( + _("FIXME: XMLUI internal action [%s] is not implemented") % action + ) + + def copy_move(source, target): + """Depending of 'action' value, copy or move from source to target.""" + if isinstance(target, ListWidget): + if isinstance(source, ListWidget): + values = source._xmlui_get_selected_values() + else: + values = [source._xmlui_get_value()] + if action == "move": + source._xmlui_set_value("") + values = [value for value in values if value] + if values: + target._xmlui_add_values(values, select=True) + else: + if isinstance(source, ListWidget): + value = ", ".join(source._xmlui_get_selected_values()) + else: + value = source._xmlui_get_value() + if action == "move": + source._xmlui_set_value("") + target._xmlui_set_value(value) + + def groups_of_contact(source, target): + """Select in target the groups of the contact which is selected in source.""" + assert isinstance(source, ListWidget) + assert isinstance(target, ListWidget) + try: + contact_jid_s = source._xmlui_get_selected_values()[0] + except IndexError: + return + target._xmlui_select_values(data[contact_jid_s]) + pass + + source = None + for field in fields: + widget = self.ctrl_list[field]["control"] + if not source: + source = widget + continue + if action in ("copy", "move"): + copy_move(source, widget) + elif action == "groups_of_contact": + groups_of_contact(source, widget) + source = None + + def get_internal_callback_data(self, action, node): + """Retrieve from node the data needed to perform given action. + + @param action (string): a value from the one that can be passed to the + 'callback' parameter of sat.tools.xml_tools.Widget.set_internal_callback + @param node (DOM Element): the node of the widget that triggers the callback + """ + # TODO: it would be better to not have a specific way to retrieve + # data for each action, but instead to have a generic method to + # extract any kind of data structure from the 'internal_data' element. + + try: # data is stored in the first 'internal_data' element of the node + data_elts = node.getElementsByTagName("internal_data")[0].childNodes + except IndexError: + return None + data = {} + if ( + action == "groups_of_contact" + ): # return a dict(key: string, value: list[string]) + for elt in data_elts: + jid_s = elt.getAttribute("name") + data[jid_s] = [] + for value_elt in elt.childNodes: + data[jid_s].append(value_elt.getAttribute("name")) + return data + + def on_form_submitted(self, ignore=None): + """An XMLUI form has been submited + + call the submit action associated with this form + """ + selected_values = [] + for ctrl_name in self.ctrl_list: + escaped = self.escape(ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl["control"], ListWidget): + selected_values.append( + (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) + ) + else: + selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) + data = dict(selected_values) + for key, value in self.hidden.items(): + data[self.escape(key)] = value + + if self.submit_id is not None: + self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + def on_form_cancelled(self, *__): + """Called when a form is cancelled""" + log.debug(_("Cancelling form")) + if self.submit_id is not None: + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + def on_save_params(self, ignore=None): + """Params are saved, we send them to backend + + self.type must be param + """ + assert self.type == "param" + for ctrl in self.param_changed: + if isinstance(ctrl, ListWidget): + value = "\t".join(ctrl._xmlui_get_selected_values()) + else: + value = ctrl._xmlui_get_value() + param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1] + self._xmlui_set_param(param_name, value, ctrl._param_category) + + self._xmlui_close() + + def show(self, *args, **kwargs): + pass + + +class AIOXMLUIPanel(XMLUIPanel): + """Asyncio compatible version of XMLUIPanel""" + + async def on_form_submitted(self, ignore=None): + """An XMLUI form has been submited + + call the submit action associated with this form + """ + selected_values = [] + for ctrl_name in self.ctrl_list: + escaped = self.escape(ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl["control"], ListWidget): + selected_values.append( + (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) + ) + else: + selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) + data = dict(selected_values) + for key, value in self.hidden.items(): + data[self.escape(key)] = value + + if self.submit_id is not None: + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + async def on_form_cancelled(self, *__): + """Called when a form is cancelled""" + log.debug(_("Cancelling form")) + if self.submit_id is not None: + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + async def submit(self, data): + self._xmlui_close() + if self.submit_id is None: + raise ValueError("Can't submit is self.submit_id is not set") + if "session_id" in data: + raise ValueError( + "session_id must no be used in data, it is automaticaly filled with " + "self.session_id if present" + ) + if self.session_id is not None: + data["session_id"] = self.session_id + await self._xmlui_launch_action(self.submit_id, data) + + async def _xmlui_launch_action(self, action_id, data): + await self.host.action_launch( + action_id, data, callback=self.callback, profile=self.profile + ) + + +class XMLUIDialog(XMLUIBase): + dialog_factory = None + + def __init__( + self, + host, + parsed_dom, + title=None, + flags=None, + callback=None, + ignore=None, + whitelist=None, + profile=C.PROF_KEY_NONE, + ): + super(XMLUIDialog, self).__init__( + host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile + ) + top = parsed_dom.documentElement + dlg_elt = self._get_child_node(top, "dialog") + if dlg_elt is None: + raise ValueError("Invalid XMLUI: no Dialog element found !") + dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE + try: + mess_elt = self._get_child_node(dlg_elt, C.XMLUI_DATA_MESS) + message = get_text(mess_elt) + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + message = "" + level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO + + if dlg_type == C.XMLUI_DIALOG_MESSAGE: + self.dlg = self.dialog_factory.createMessageDialog( + self, self.xmlui_title, message, level + ) + elif dlg_type == C.XMLUI_DIALOG_NOTE: + self.dlg = self.dialog_factory.createNoteDialog( + self, self.xmlui_title, message, level + ) + elif dlg_type == C.XMLUI_DIALOG_CONFIRM: + try: + buttons_elt = self._get_child_node(dlg_elt, "buttons") + buttons_set = ( + buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT + ) + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT + self.dlg = self.dialog_factory.createConfirmDialog( + self, self.xmlui_title, message, level, buttons_set + ) + elif dlg_type == C.XMLUI_DIALOG_FILE: + try: + file_elt = self._get_child_node(dlg_elt, "file") + filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + filetype = C.XMLUI_DATA_FILETYPE_DEFAULT + self.dlg = self.dialog_factory.createFileDialog( + self, self.xmlui_title, message, level, filetype + ) + else: + raise ValueError("Unknown dialog type [%s]" % dlg_type) + + def show(self): + self.dlg._xmlui_show() + + def _xmlui_close(self): + self.dlg._xmlui_close() + + +def register_class(type_, class_): + """Register the class to use with the factory + + @param type_: one of: + CLASS_PANEL: classical XMLUI interface + CLASS_DIALOG: XMLUI dialog + @param class_: the class to use to instanciate given type + """ + # TODO: remove this method, as there are seme use cases where different XMLUI + # classes can be used in the same frontend, so a global value is not good + assert type_ in (CLASS_PANEL, CLASS_DIALOG) + log.warning("register_class for XMLUI is deprecated, please use partial with " + "xmlui.create and class_map instead") + if type_ in _class_map: + log.debug(_("XMLUI class already registered for {type_}, ignoring").format( + type_=type_)) + return + + _class_map[type_] = class_ + + +def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, + callback=None, ignore=None, whitelist=None, class_map=None, + profile=C.PROF_KEY_NONE): + """ + @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one + @param dom_free: method used to free the parsed DOM + @param ignore(list[unicode], None): name of widgets to ignore + widgets with name in this list and their label will be ignored + @param whitelist(list[unicode], None): name of widgets to keep + when not None, only widgets in this list and their label will be kept + mutually exclusive with ignore + """ + if class_map is None: + class_map = _class_map + if dom_parse is None: + from xml.dom import minidom + + dom_parse = lambda xml_data: minidom.parseString(xml_data.encode("utf-8")) + dom_free = lambda parsed_dom: parsed_dom.unlink() + else: + dom_parse = dom_parse + dom_free = dom_free or (lambda parsed_dom: None) + parsed_dom = dom_parse(xml_data) + top = parsed_dom.documentElement + ui_type = top.getAttribute("type") + try: + if ui_type != C.XMLUI_DIALOG: + cls = class_map[CLASS_PANEL] + else: + cls = class_map[CLASS_DIALOG] + except KeyError: + raise ClassNotRegistedError( + _("You must register classes with register_class before creating a XMLUI") + ) + + xmlui = cls( + host, + parsed_dom, + title=title, + flags=flags, + callback=callback, + ignore=ignore, + whitelist=whitelist, + profile=profile, + ) + dom_free(parsed_dom) + return xmlui diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/bridge/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/bridge/bridge_frontend.py --- a/sat_frontends/bridge/bridge_frontend.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - - -# SAT communication bridge -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -class BridgeException(Exception): - """An exception which has been raised from the backend and arrived to the frontend.""" - - def __init__(self, name, message="", condition=""): - """ - - @param name (str): full exception class name (with module) - @param message (str): error message - @param condition (str) : error condition - """ - super().__init__() - self.fullname = str(name) - self.message = str(message) - self.condition = str(condition) if condition else "" - self.module, __, self.classname = str(self.fullname).rpartition(".") - - def __str__(self): - return self.classname + (f": {self.message}" if self.message else "") - - def __eq__(self, other): - return self.classname == other diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/bridge/dbus_bridge.py --- a/sat_frontends/bridge/dbus_bridge.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1512 +0,0 @@ -#!/usr/bin/env python3 - -# SàT communication bridge -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import asyncio -import dbus -import ast -from libervia.backend.core.i18n import _ -from libervia.backend.tools import config -from libervia.backend.core.log import getLogger -from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError -from dbus.mainloop.glib import DBusGMainLoop -from .bridge_frontend import BridgeException - - -DBusGMainLoop(set_as_default=True) -log = getLogger(__name__) - - -# Interface prefix -const_INT_PREFIX = config.config_get( - config.parse_main_conf(), - "", - "bridge_dbus_int_prefix", - "org.libervia.Libervia") -const_ERROR_PREFIX = const_INT_PREFIX + ".error" -const_OBJ_PATH = '/org/libervia/Libervia/bridge' -const_CORE_SUFFIX = ".core" -const_PLUGIN_SUFFIX = ".plugin" -const_TIMEOUT = 120 - - -def dbus_to_bridge_exception(dbus_e): - """Convert a DBusException to a BridgeException. - - @param dbus_e (DBusException) - @return: BridgeException - """ - full_name = dbus_e.get_dbus_name() - if full_name.startswith(const_ERROR_PREFIX): - name = dbus_e.get_dbus_name()[len(const_ERROR_PREFIX) + 1:] - else: - name = full_name - # XXX: dbus_e.args doesn't contain the original DBusException args, but we - # receive its serialized form in dbus_e.args[0]. From that we can rebuild - # the original arguments list thanks to ast.literal_eval (secure eval). - message = dbus_e.get_dbus_message() # similar to dbus_e.args[0] - try: - message, condition = ast.literal_eval(message) - except (SyntaxError, ValueError, TypeError): - condition = '' - return BridgeException(name, message, condition) - - -class bridge: - - def bridge_connect(self, callback, errback): - try: - self.sessions_bus = dbus.SessionBus() - self.db_object = self.sessions_bus.get_object(const_INT_PREFIX, - const_OBJ_PATH) - self.db_core_iface = dbus.Interface(self.db_object, - dbus_interface=const_INT_PREFIX + const_CORE_SUFFIX) - self.db_plugin_iface = dbus.Interface(self.db_object, - dbus_interface=const_INT_PREFIX + const_PLUGIN_SUFFIX) - except dbus.exceptions.DBusException as e: - if e._dbus_error_name in ('org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Spawn.ExecFailed'): - errback(BridgeExceptionNoService()) - elif e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': - log.error(_("D-Bus is not launched, please see README to see instructions on how to launch it")) - errback(BridgeInitError) - else: - errback(e) - else: - callback() - #props = self.db_core_iface.getProperties() - - def register_signal(self, functionName, handler, iface="core"): - if iface == "core": - self.db_core_iface.connect_to_signal(functionName, handler) - elif iface == "plugin": - self.db_plugin_iface.connect_to_signal(functionName, handler) - else: - log.error(_('Unknown interface')) - - def __getattribute__(self, name): - """ usual __getattribute__ if the method exists, else try to find a plugin method """ - try: - return object.__getattribute__(self, name) - except AttributeError: - # The attribute is not found, we try the plugin proxy to find the requested method - - def get_plugin_method(*args, **kwargs): - # We first check if we have an async call. We detect this in two ways: - # - if we have the 'callback' and 'errback' keyword arguments - # - or if the last two arguments are callable - - async_ = False - args = list(args) - - if kwargs: - if 'callback' in kwargs: - async_ = True - _callback = kwargs.pop('callback') - _errback = kwargs.pop('errback', lambda failure: log.error(str(failure))) - try: - args.append(kwargs.pop('profile')) - except KeyError: - try: - args.append(kwargs.pop('profile_key')) - except KeyError: - pass - # at this point, kwargs should be empty - if kwargs: - log.warning("unexpected keyword arguments, they will be ignored: {}".format(kwargs)) - elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): - async_ = True - _errback = args.pop() - _callback = args.pop() - - method = getattr(self.db_plugin_iface, name) - - if async_: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = _callback - kwargs['error_handler'] = lambda err: _errback(dbus_to_bridge_exception(err)) - - try: - return method(*args, **kwargs) - except ValueError as e: - if e.args[0].startswith("Unable to guess signature"): - # XXX: if frontend is started too soon after backend, the - # inspection misses methods (notably plugin dynamically added - # methods). The following hack works around that by redoing the - # cache of introspected methods signatures. - log.debug("using hack to work around inspection issue") - proxy = self.db_plugin_iface.proxy_object - IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS - proxy._introspect_state = IN_PROGRESS - proxy._Introspect() - return self.db_plugin_iface.get_dbus_method(name)(*args, **kwargs) - raise e - - return get_plugin_method - - def action_launch(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.action_launch(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def actions_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.actions_get(profile_key, **kwargs) - - def config_get(self, section, name, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.config_get(section, name, **kwargs)) - - def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.connect(profile_key, password, options, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def contact_add(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.contact_add(entity_jid, profile_key, **kwargs) - - def contact_del(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.contact_del(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def contact_get(self, arg_0, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.contact_get(arg_0, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.contact_update(entity_jid, name, groups, profile_key, **kwargs) - - def contacts_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.contacts_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def contacts_get_from_group(self, group, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.contacts_get_from_group(group, profile_key, **kwargs) - - def devices_infos_get(self, bare_jid, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.devices_infos_get(bare_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.disco_find_by_features(namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.disco_infos(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.disco_items(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def encryption_namespace_get(self, arg_0, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.encryption_namespace_get(arg_0, **kwargs)) - - def encryption_plugins_get(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.encryption_plugins_get(**kwargs)) - - def encryption_trust_ui_get(self, to_jid, namespace, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.encryption_trust_ui_get(to_jid, namespace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def entities_data_get(self, jids, keys, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.entities_data_get(jids, keys, profile, **kwargs) - - def entity_data_get(self, jid, keys, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.entity_data_get(jid, keys, profile, **kwargs) - - def features_get(self, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.features_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.history_get(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def image_check(self, arg_0, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.image_check(arg_0, **kwargs)) - - def image_convert(self, source, dest, arg_2, extra, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.image_convert(source, dest, arg_2, extra, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def image_generate_preview(self, image_path, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.image_generate_preview(image_path, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def image_resize(self, image_path, width, height, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.image_resize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def is_connected(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.is_connected(profile_key, **kwargs) - - def main_resource_get(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.main_resource_get(contact_jid, profile_key, **kwargs)) - - def menu_help_get(self, menu_id, language, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.menu_help_get(menu_id, language, **kwargs)) - - def menu_launch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.menu_launch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def menus_get(self, language, security_limit, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.menus_get(language, security_limit, **kwargs) - - def message_encryption_get(self, to_jid, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.message_encryption_get(to_jid, profile_key, **kwargs)) - - def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.message_encryption_start(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def message_encryption_stop(self, to_jid, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.message_encryption_stop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.message_send(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def namespaces_get(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.namespaces_get(**kwargs) - - def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.param_get_a(name, category, attribute, profile_key, **kwargs)) - - def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.param_get_a_async(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.param_set(name, value, category, security_limit, profile_key, **kwargs) - - def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.param_ui_get(security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def params_categories_get(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.params_categories_get(**kwargs) - - def params_register_app(self, xml, security_limit=-1, app='', callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.params_register_app(xml, security_limit, app, **kwargs) - - def params_template_load(self, filename, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.params_template_load(filename, **kwargs) - - def params_template_save(self, filename, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.params_template_save(filename, **kwargs) - - def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.params_values_from_category_get_async(category, security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.presence_set(to_jid, show, statuses, profile_key, **kwargs) - - def presence_statuses_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.presence_statuses_get(profile_key, **kwargs) - - def private_data_delete(self, namespace, key, arg_2, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.private_data_delete(namespace, key, arg_2, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def private_data_get(self, namespace, key, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return str(self.db_core_iface.private_data_get(namespace, key, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def private_data_set(self, namespace, key, data, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.private_data_set(namespace, key, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profile_create(self, profile, password='', component='', callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.profile_create(profile, password, component, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profile_delete_async(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.profile_delete_async(profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profile_is_session_started(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profile_is_session_started(profile_key, **kwargs) - - def profile_name_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.profile_name_get(profile_key, **kwargs)) - - def profile_set_default(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profile_set_default(profile, **kwargs) - - def profile_start_session(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.profile_start_session(password, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profiles_list_get(self, clients=True, components=False, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profiles_list_get(clients, components, **kwargs) - - def progress_get(self, id, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progress_get(id, profile, **kwargs) - - def progress_get_all(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progress_get_all(profile, **kwargs) - - def progress_get_all_metadata(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progress_get_all_metadata(profile, **kwargs) - - def ready_get(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.ready_get(timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def roster_resync(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.roster_resync(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def session_infos_get(self, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.session_infos_get(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def sub_waiting_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.sub_waiting_get(profile_key, **kwargs) - - def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs) - - def version_get(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return str(self.db_core_iface.version_get(**kwargs)) - - -class AIOBridge(bridge): - - def register_signal(self, functionName, handler, iface="core"): - loop = asyncio.get_running_loop() - async_handler = lambda *args: asyncio.run_coroutine_threadsafe(handler(*args), loop) - return super().register_signal(functionName, async_handler, iface) - - def __getattribute__(self, name): - """ usual __getattribute__ if the method exists, else try to find a plugin method """ - try: - return object.__getattribute__(self, name) - except AttributeError: - # The attribute is not found, we try the plugin proxy to find the requested method - def get_plugin_method(*args, **kwargs): - loop = asyncio.get_running_loop() - fut = loop.create_future() - method = getattr(self.db_plugin_iface, name) - reply_handler = lambda ret=None: loop.call_soon_threadsafe( - fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe( - fut.set_exception, dbus_to_bridge_exception(err)) - try: - method( - *args, - **kwargs, - timeout=const_TIMEOUT, - reply_handler=reply_handler, - error_handler=error_handler - ) - except ValueError as e: - if e.args[0].startswith("Unable to guess signature"): - # same hack as for bridge.__getattribute__ - log.warning("using hack to work around inspection issue") - proxy = self.db_plugin_iface.proxy_object - IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS - proxy._introspect_state = IN_PROGRESS - proxy._Introspect() - self.db_plugin_iface.get_dbus_method(name)( - *args, - **kwargs, - timeout=const_TIMEOUT, - reply_handler=reply_handler, - error_handler=error_handler - ) - - else: - raise e - return fut - - return get_plugin_method - - def bridge_connect(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - super().bridge_connect( - callback=lambda: loop.call_soon_threadsafe(fut.set_result, None), - errback=lambda e: loop.call_soon_threadsafe(fut.set_exception, e) - ) - return fut - - def action_launch(self, callback_id, data, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.action_launch(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def actions_get(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.actions_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def config_get(self, section, name): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.config_get(section, name, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def connect(self, profile_key="@DEFAULT@", password='', options={}): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.connect(profile_key, password, options, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contact_add(self, entity_jid, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contact_add(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contact_del(self, entity_jid, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contact_del(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contact_get(self, arg_0, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contact_get(arg_0, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contact_update(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contacts_get(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contacts_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def contacts_get_from_group(self, group, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.contacts_get_from_group(group, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def devices_infos_get(self, bare_jid, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.devices_infos_get(bare_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.disco_find_by_features(namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.disco_infos(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.disco_items(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def disconnect(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def encryption_namespace_get(self, arg_0): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.encryption_namespace_get(arg_0, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def encryption_plugins_get(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.encryption_plugins_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def encryption_trust_ui_get(self, to_jid, namespace, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.encryption_trust_ui_get(to_jid, namespace, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def entities_data_get(self, jids, keys, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.entities_data_get(jids, keys, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def entity_data_get(self, jid, keys, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.entity_data_get(jid, keys, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def features_get(self, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.features_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.history_get(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def image_check(self, arg_0): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.image_check(arg_0, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def image_convert(self, source, dest, arg_2, extra): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.image_convert(source, dest, arg_2, extra, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def image_generate_preview(self, image_path, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.image_generate_preview(image_path, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def image_resize(self, image_path, width, height): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.image_resize(image_path, width, height, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def is_connected(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.is_connected(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.main_resource_get(contact_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def menu_help_get(self, menu_id, language): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.menu_help_get(menu_id, language, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def menu_launch(self, menu_type, path, data, security_limit, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.menu_launch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def menus_get(self, language, security_limit): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.menus_get(language, security_limit, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def message_encryption_get(self, to_jid, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.message_encryption_get(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.message_encryption_start(to_jid, namespace, replace, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def message_encryption_stop(self, to_jid, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.message_encryption_stop(to_jid, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.message_send(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def namespaces_get(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.namespaces_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.param_get_a(name, category, attribute, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.param_get_a_async(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.param_set(name, value, category, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.param_ui_get(security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def params_categories_get(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.params_categories_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def params_register_app(self, xml, security_limit=-1, app=''): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.params_register_app(xml, security_limit, app, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def params_template_load(self, filename): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.params_template_load(filename, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def params_template_save(self, filename): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.params_template_save(filename, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.params_values_from_category_get_async(category, security_limit, app, extra, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.presence_set(to_jid, show, statuses, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def presence_statuses_get(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.presence_statuses_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def private_data_delete(self, namespace, key, arg_2): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.private_data_delete(namespace, key, arg_2, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def private_data_get(self, namespace, key, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.private_data_get(namespace, key, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def private_data_set(self, namespace, key, data, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.private_data_set(namespace, key, data, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_create(self, profile, password='', component=''): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_create(profile, password, component, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_delete_async(self, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_delete_async(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_is_session_started(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_is_session_started(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_name_get(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_name_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_set_default(self, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_set_default(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profile_start_session(self, password='', profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profile_start_session(password, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def profiles_list_get(self, clients=True, components=False): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.profiles_list_get(clients, components, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def progress_get(self, id, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.progress_get(id, profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def progress_get_all(self, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.progress_get_all(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def progress_get_all_metadata(self, profile): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.progress_get_all_metadata(profile, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def ready_get(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.ready_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def roster_resync(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.roster_resync(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def session_infos_get(self, profile_key): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.session_infos_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def sub_waiting_get(self, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.sub_waiting_get(profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.subscription(sub_type, entity, profile_key, timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut - - def version_get(self): - loop = asyncio.get_running_loop() - fut = loop.create_future() - reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) - error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) - self.db_core_iface.version_get(timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) - return fut diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/bridge/pb.py --- a/sat_frontends/bridge/pb.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1120 +0,0 @@ -#!/usr/bin/env python3 - -# SàT communication bridge -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import asyncio -from logging import getLogger -from functools import partial -from pathlib import Path -from twisted.spread import pb -from twisted.internet import reactor, defer -from twisted.internet.error import ConnectionRefusedError, ConnectError -from libervia.backend.core import exceptions -from libervia.backend.tools import config -from sat_frontends.bridge.bridge_frontend import BridgeException - -log = getLogger(__name__) - - -class SignalsHandler(pb.Referenceable): - def __getattr__(self, name): - if name.startswith("remote_"): - log.debug("calling an unregistered signal: {name}".format(name=name[7:])) - return lambda *args, **kwargs: None - - else: - raise AttributeError(name) - - def register_signal(self, name, handler, iface="core"): - log.debug("registering signal {name}".format(name=name)) - method_name = "remote_" + name - try: - self.__getattribute__(method_name) - except AttributeError: - pass - else: - raise exceptions.InternalError( - "{name} signal handler has been registered twice".format( - name=method_name - ) - ) - setattr(self, method_name, handler) - - -class bridge(object): - - def __init__(self): - self.signals_handler = SignalsHandler() - - def __getattr__(self, name): - return partial(self.call, name) - - def _generic_errback(self, err): - log.error(f"bridge error: {err}") - - def _errback(self, failure_, ori_errback): - """Convert Failure to BridgeException""" - ori_errback( - BridgeException( - name=failure_.type.decode('utf-8'), - message=str(failure_.value) - ) - ) - - def remote_callback(self, result, callback): - """call callback with argument or None - - if result is not None not argument is used, - else result is used as argument - @param result: remote call result - @param callback(callable): method to call on result - """ - if result is None: - callback() - else: - callback(result) - - def call(self, name, *args, **kwargs): - """call a remote method - - @param name(str): name of the bridge method - @param args(list): arguments - may contain callback and errback as last 2 items - @param kwargs(dict): keyword arguments - may contain callback and errback - """ - callback = errback = None - if kwargs: - try: - callback = kwargs.pop("callback") - except KeyError: - pass - try: - errback = kwargs.pop("errback") - except KeyError: - pass - elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): - errback = args.pop() - callback = args.pop() - d = self.root.callRemote(name, *args, **kwargs) - if callback is not None: - d.addCallback(self.remote_callback, callback) - if errback is not None: - d.addErrback(errback) - - def _init_bridge_eb(self, failure_): - log.error("Can't init bridge: {msg}".format(msg=failure_)) - return failure_ - - def _set_root(self, root): - """set remote root object - - bridge will then be initialised - """ - self.root = root - d = root.callRemote("initBridge", self.signals_handler) - d.addErrback(self._init_bridge_eb) - return d - - def get_root_object_eb(self, failure_): - """Call errback with appropriate bridge error""" - if failure_.check(ConnectionRefusedError, ConnectError): - raise exceptions.BridgeExceptionNoService - else: - raise failure_ - - def bridge_connect(self, callback, errback): - factory = pb.PBClientFactory() - conf = config.parse_main_conf() - get_conf = partial(config.get_conf, conf, "bridge_pb", "") - conn_type = get_conf("connection_type", "unix_socket") - if conn_type == "unix_socket": - local_dir = Path(config.config_get(conf, "", "local_dir")).resolve() - socket_path = local_dir / "bridge_pb" - reactor.connectUNIX(str(socket_path), factory) - elif conn_type == "socket": - host = get_conf("host", "localhost") - port = int(get_conf("port", 8789)) - reactor.connectTCP(host, port, factory) - else: - raise ValueError(f"Unknown pb connection type: {conn_type!r}") - d = factory.getRootObject() - d.addCallback(self._set_root) - if callback is not None: - d.addCallback(lambda __: callback()) - d.addErrback(self.get_root_object_eb) - if errback is not None: - d.addErrback(lambda failure_: errback(failure_.value)) - return d - - def register_signal(self, functionName, handler, iface="core"): - self.signals_handler.register_signal(functionName, handler, iface) - - - def action_launch(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("action_launch", callback_id, data, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def actions_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("actions_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def config_get(self, section, name, callback=None, errback=None): - d = self.root.callRemote("config_get", section, name) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): - d = self.root.callRemote("connect", profile_key, password, options) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contact_add(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contact_add", entity_jid, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contact_del(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contact_del", entity_jid, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contact_get(self, arg_0, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contact_get", arg_0, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contact_update", entity_jid, name, groups, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contacts_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contacts_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def contacts_get_from_group(self, group, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("contacts_get_from_group", group, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def devices_infos_get(self, bare_jid, profile_key, callback=None, errback=None): - d = self.root.callRemote("devices_infos_get", bare_jid, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("disco_find_by_features", namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("disco_infos", entity_jid, node, use_cache, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("disco_items", entity_jid, node, use_cache, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("disconnect", profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def encryption_namespace_get(self, arg_0, callback=None, errback=None): - d = self.root.callRemote("encryption_namespace_get", arg_0) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def encryption_plugins_get(self, callback=None, errback=None): - d = self.root.callRemote("encryption_plugins_get") - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def encryption_trust_ui_get(self, to_jid, namespace, profile_key, callback=None, errback=None): - d = self.root.callRemote("encryption_trust_ui_get", to_jid, namespace, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def entities_data_get(self, jids, keys, profile, callback=None, errback=None): - d = self.root.callRemote("entities_data_get", jids, keys, profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def entity_data_get(self, jid, keys, profile, callback=None, errback=None): - d = self.root.callRemote("entity_data_get", jid, keys, profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def features_get(self, profile_key, callback=None, errback=None): - d = self.root.callRemote("features_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): - d = self.root.callRemote("history_get", from_jid, to_jid, limit, between, filters, profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def image_check(self, arg_0, callback=None, errback=None): - d = self.root.callRemote("image_check", arg_0) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def image_convert(self, source, dest, arg_2, extra, callback=None, errback=None): - d = self.root.callRemote("image_convert", source, dest, arg_2, extra) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def image_generate_preview(self, image_path, profile_key, callback=None, errback=None): - d = self.root.callRemote("image_generate_preview", image_path, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def image_resize(self, image_path, width, height, callback=None, errback=None): - d = self.root.callRemote("image_resize", image_path, width, height) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def is_connected(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("is_connected", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def main_resource_get(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("main_resource_get", contact_jid, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def menu_help_get(self, menu_id, language, callback=None, errback=None): - d = self.root.callRemote("menu_help_get", menu_id, language) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def menu_launch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): - d = self.root.callRemote("menu_launch", menu_type, path, data, security_limit, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def menus_get(self, language, security_limit, callback=None, errback=None): - d = self.root.callRemote("menus_get", language, security_limit) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def message_encryption_get(self, to_jid, profile_key, callback=None, errback=None): - d = self.root.callRemote("message_encryption_get", to_jid, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@", callback=None, errback=None): - d = self.root.callRemote("message_encryption_start", to_jid, namespace, replace, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def message_encryption_stop(self, to_jid, profile_key, callback=None, errback=None): - d = self.root.callRemote("message_encryption_stop", to_jid, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - d = self.root.callRemote("message_send", to_jid, message, subject, mess_type, extra, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def namespaces_get(self, callback=None, errback=None): - d = self.root.callRemote("namespaces_get") - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("param_get_a", name, category, attribute, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("param_get_a_async", name, category, attribute, security_limit, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("param_set", name, value, category, security_limit, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("param_ui_get", security_limit, app, extra, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def params_categories_get(self, callback=None, errback=None): - d = self.root.callRemote("params_categories_get") - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def params_register_app(self, xml, security_limit=-1, app='', callback=None, errback=None): - d = self.root.callRemote("params_register_app", xml, security_limit, app) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def params_template_load(self, filename, callback=None, errback=None): - d = self.root.callRemote("params_template_load", filename) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def params_template_save(self, filename, callback=None, errback=None): - d = self.root.callRemote("params_template_save", filename) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("params_values_from_category_get_async", category, security_limit, app, extra, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("presence_set", to_jid, show, statuses, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def presence_statuses_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("presence_statuses_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def private_data_delete(self, namespace, key, arg_2, callback=None, errback=None): - d = self.root.callRemote("private_data_delete", namespace, key, arg_2) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def private_data_get(self, namespace, key, profile_key, callback=None, errback=None): - d = self.root.callRemote("private_data_get", namespace, key, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def private_data_set(self, namespace, key, data, profile_key, callback=None, errback=None): - d = self.root.callRemote("private_data_set", namespace, key, data, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_create(self, profile, password='', component='', callback=None, errback=None): - d = self.root.callRemote("profile_create", profile, password, component) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_delete_async(self, profile, callback=None, errback=None): - d = self.root.callRemote("profile_delete_async", profile) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_is_session_started(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("profile_is_session_started", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_name_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("profile_name_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_set_default(self, profile, callback=None, errback=None): - d = self.root.callRemote("profile_set_default", profile) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profile_start_session(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("profile_start_session", password, profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def profiles_list_get(self, clients=True, components=False, callback=None, errback=None): - d = self.root.callRemote("profiles_list_get", clients, components) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def progress_get(self, id, profile, callback=None, errback=None): - d = self.root.callRemote("progress_get", id, profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def progress_get_all(self, profile, callback=None, errback=None): - d = self.root.callRemote("progress_get_all", profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def progress_get_all_metadata(self, profile, callback=None, errback=None): - d = self.root.callRemote("progress_get_all_metadata", profile) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def ready_get(self, callback=None, errback=None): - d = self.root.callRemote("ready_get") - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def roster_resync(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("roster_resync", profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def session_infos_get(self, profile_key, callback=None, errback=None): - d = self.root.callRemote("session_infos_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def sub_waiting_get(self, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("sub_waiting_get", profile_key) - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): - d = self.root.callRemote("subscription", sub_type, entity, profile_key) - if callback is not None: - d.addCallback(lambda __: callback()) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - def version_get(self, callback=None, errback=None): - d = self.root.callRemote("version_get") - if callback is not None: - d.addCallback(callback) - if errback is None: - d.addErrback(self._generic_errback) - else: - d.addErrback(self._errback, ori_errback=errback) - - -class AIOSignalsHandler(SignalsHandler): - - def register_signal(self, name, handler, iface="core"): - async_handler = lambda *args, **kwargs: defer.Deferred.fromFuture( - asyncio.ensure_future(handler(*args, **kwargs))) - return super().register_signal(name, async_handler, iface) - - -class AIOBridge(bridge): - - def __init__(self): - self.signals_handler = AIOSignalsHandler() - - def _errback(self, failure_): - """Convert Failure to BridgeException""" - raise BridgeException( - name=failure_.type.decode('utf-8'), - message=str(failure_.value) - ) - - def call(self, name, *args, **kwargs): - d = self.root.callRemote(name, *args, *kwargs) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - async def bridge_connect(self): - d = super().bridge_connect(callback=None, errback=None) - return await d.asFuture(asyncio.get_event_loop()) - - def action_launch(self, callback_id, data, profile_key="@DEFAULT@"): - d = self.root.callRemote("action_launch", callback_id, data, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def actions_get(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("actions_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def config_get(self, section, name): - d = self.root.callRemote("config_get", section, name) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def connect(self, profile_key="@DEFAULT@", password='', options={}): - d = self.root.callRemote("connect", profile_key, password, options) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contact_add(self, entity_jid, profile_key="@DEFAULT@"): - d = self.root.callRemote("contact_add", entity_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contact_del(self, entity_jid, profile_key="@DEFAULT@"): - d = self.root.callRemote("contact_del", entity_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contact_get(self, arg_0, profile_key="@DEFAULT@"): - d = self.root.callRemote("contact_get", arg_0, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"): - d = self.root.callRemote("contact_update", entity_jid, name, groups, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contacts_get(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("contacts_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def contacts_get_from_group(self, group, profile_key="@DEFAULT@"): - d = self.root.callRemote("contacts_get_from_group", group, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def devices_infos_get(self, bare_jid, profile_key): - d = self.root.callRemote("devices_infos_get", bare_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@"): - d = self.root.callRemote("disco_find_by_features", namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): - d = self.root.callRemote("disco_infos", entity_jid, node, use_cache, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): - d = self.root.callRemote("disco_items", entity_jid, node, use_cache, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def disconnect(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("disconnect", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def encryption_namespace_get(self, arg_0): - d = self.root.callRemote("encryption_namespace_get", arg_0) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def encryption_plugins_get(self): - d = self.root.callRemote("encryption_plugins_get") - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def encryption_trust_ui_get(self, to_jid, namespace, profile_key): - d = self.root.callRemote("encryption_trust_ui_get", to_jid, namespace, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def entities_data_get(self, jids, keys, profile): - d = self.root.callRemote("entities_data_get", jids, keys, profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def entity_data_get(self, jid, keys, profile): - d = self.root.callRemote("entity_data_get", jid, keys, profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def features_get(self, profile_key): - d = self.root.callRemote("features_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@"): - d = self.root.callRemote("history_get", from_jid, to_jid, limit, between, filters, profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def image_check(self, arg_0): - d = self.root.callRemote("image_check", arg_0) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def image_convert(self, source, dest, arg_2, extra): - d = self.root.callRemote("image_convert", source, dest, arg_2, extra) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def image_generate_preview(self, image_path, profile_key): - d = self.root.callRemote("image_generate_preview", image_path, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def image_resize(self, image_path, width, height): - d = self.root.callRemote("image_resize", image_path, width, height) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def is_connected(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("is_connected", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def main_resource_get(self, contact_jid, profile_key="@DEFAULT@"): - d = self.root.callRemote("main_resource_get", contact_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def menu_help_get(self, menu_id, language): - d = self.root.callRemote("menu_help_get", menu_id, language) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def menu_launch(self, menu_type, path, data, security_limit, profile_key): - d = self.root.callRemote("menu_launch", menu_type, path, data, security_limit, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def menus_get(self, language, security_limit): - d = self.root.callRemote("menus_get", language, security_limit) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def message_encryption_get(self, to_jid, profile_key): - d = self.root.callRemote("message_encryption_get", to_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): - d = self.root.callRemote("message_encryption_start", to_jid, namespace, replace, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def message_encryption_stop(self, to_jid, profile_key): - d = self.root.callRemote("message_encryption_stop", to_jid, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@"): - d = self.root.callRemote("message_send", to_jid, message, subject, mess_type, extra, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def namespaces_get(self): - d = self.root.callRemote("namespaces_get") - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"): - d = self.root.callRemote("param_get_a", name, category, attribute, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@"): - d = self.root.callRemote("param_get_a_async", name, category, attribute, security_limit, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): - d = self.root.callRemote("param_set", name, value, category, security_limit, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"): - d = self.root.callRemote("param_ui_get", security_limit, app, extra, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def params_categories_get(self): - d = self.root.callRemote("params_categories_get") - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def params_register_app(self, xml, security_limit=-1, app=''): - d = self.root.callRemote("params_register_app", xml, security_limit, app) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def params_template_load(self, filename): - d = self.root.callRemote("params_template_load", filename) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def params_template_save(self, filename): - d = self.root.callRemote("params_template_save", filename) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"): - d = self.root.callRemote("params_values_from_category_get_async", category, security_limit, app, extra, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): - d = self.root.callRemote("presence_set", to_jid, show, statuses, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def presence_statuses_get(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("presence_statuses_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def private_data_delete(self, namespace, key, arg_2): - d = self.root.callRemote("private_data_delete", namespace, key, arg_2) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def private_data_get(self, namespace, key, profile_key): - d = self.root.callRemote("private_data_get", namespace, key, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def private_data_set(self, namespace, key, data, profile_key): - d = self.root.callRemote("private_data_set", namespace, key, data, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_create(self, profile, password='', component=''): - d = self.root.callRemote("profile_create", profile, password, component) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_delete_async(self, profile): - d = self.root.callRemote("profile_delete_async", profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_is_session_started(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("profile_is_session_started", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_name_get(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("profile_name_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_set_default(self, profile): - d = self.root.callRemote("profile_set_default", profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profile_start_session(self, password='', profile_key="@DEFAULT@"): - d = self.root.callRemote("profile_start_session", password, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def profiles_list_get(self, clients=True, components=False): - d = self.root.callRemote("profiles_list_get", clients, components) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def progress_get(self, id, profile): - d = self.root.callRemote("progress_get", id, profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def progress_get_all(self, profile): - d = self.root.callRemote("progress_get_all", profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def progress_get_all_metadata(self, profile): - d = self.root.callRemote("progress_get_all_metadata", profile) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def ready_get(self): - d = self.root.callRemote("ready_get") - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def roster_resync(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("roster_resync", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def session_infos_get(self, profile_key): - d = self.root.callRemote("session_infos_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def sub_waiting_get(self, profile_key="@DEFAULT@"): - d = self.root.callRemote("sub_waiting_get", profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): - d = self.root.callRemote("subscription", sub_type, entity, profile_key) - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) - - def version_get(self): - d = self.root.callRemote("version_get") - d.addErrback(self._errback) - return d.asFuture(asyncio.get_event_loop()) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/__init__.py diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/arg_tools.py --- a/sat_frontends/jp/arg_tools.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -from libervia.backend.core import exceptions - - -def escape(arg, smart=True): - """format arg with quotes - - @param smart(bool): if True, only escape if needed - """ - if smart and not " " in arg and not '"' in arg: - return arg - return '"' + arg.replace('"', '\\"') + '"' - - -def get_cmd_choices(cmd=None, parser=None): - try: - choices = parser._subparsers._group_actions[0].choices - return choices[cmd] if cmd is not None else choices - except (KeyError, AttributeError): - raise exceptions.NotFound - - -def get_use_args(host, args, use, verbose=False, parser=None): - """format args for argparse parser with values prefilled - - @param host(JP): jp instance - @param args(list(str)): arguments to use - @param use(dict[str, str]): arguments to fill if found in parser - @param verbose(bool): if True a message will be displayed when argument is used or not - @param parser(argparse.ArgumentParser): parser to use - @return (tuple[list[str],list[str]]): 2 args lists: - - parser args, i.e. given args corresponding to parsers - - use args, i.e. generated args from use - """ - # FIXME: positional args are not handled correclty - # if there is more that one, the position is not corrected - if parser is None: - parser = host.parser - - # we check not optional args to see if there - # is a corresonding parser - # else USE args would not work correctly (only for current parser) - parser_args = [] - for arg in args: - if arg.startswith("-"): - break - try: - parser = get_cmd_choices(arg, parser) - except exceptions.NotFound: - break - parser_args.append(arg) - - # post_args are remaning given args, - # without the ones corresponding to parsers - post_args = args[len(parser_args) :] - - opt_args = [] - pos_args = [] - actions = {a.dest: a for a in parser._actions} - for arg, value in use.items(): - try: - if arg == "item" and not "item" in actions: - # small hack when --item is appended to a --items list - arg = "items" - action = actions[arg] - except KeyError: - if verbose: - host.disp( - _( - "ignoring {name}={value}, not corresponding to any argument (in USE)" - ).format(name=arg, value=escape(value)) - ) - else: - if verbose: - host.disp( - _("arg {name}={value} (in USE)").format( - name=arg, value=escape(value) - ) - ) - if not action.option_strings: - pos_args.append(value) - else: - opt_args.append(action.option_strings[0]) - opt_args.append(value) - return parser_args, opt_args + pos_args + post_args diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/base.py --- a/sat_frontends/jp/base.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1435 +0,0 @@ -#!/usr/bin/env python3 - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import asyncio -from libervia.backend.core.i18n import _ - -### logging ### -import logging as log -log.basicConfig(level=log.WARNING, - format='[%(name)s] %(message)s') -### - -import sys -import os -import os.path -import argparse -import inspect -import tty -import termios -from pathlib import Path -from glob import iglob -from typing import Optional, Set, Union -from importlib import import_module -from sat_frontends.tools.jid import JID -from libervia.backend.tools import config -from libervia.backend.tools.common import dynamic_import -from libervia.backend.tools.common import uri -from libervia.backend.tools.common import date_utils -from libervia.backend.tools.common import utils -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.core import exceptions -import sat_frontends.jp -from sat_frontends.jp.loops import QuitException, get_jp_loop -from sat_frontends.jp.constants import Const as C -from sat_frontends.bridge.bridge_frontend import BridgeException -from sat_frontends.tools import misc -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -from collections import OrderedDict - -## bridge handling -# we get bridge name from conf and initialise the right class accordingly -main_config = config.parse_main_conf() -bridge_name = config.config_get(main_config, '', 'bridge', 'dbus') -JPLoop = get_jp_loop(bridge_name) - - -try: - import progressbar -except ImportError: - msg = (_('ProgressBar not available, please download it at ' - 'http://pypi.python.org/pypi/progressbar\n' - 'Progress bar deactivated\n--\n')) - print(msg, file=sys.stderr) - progressbar=None - -#consts -DESCRIPTION = """This software is a command line tool for XMPP. -Get the latest version at """ + C.APP_URL - -COPYLEFT = """Copyright (C) 2009-2021 Jérôme Poisson, Adrien Cossa -This program comes with ABSOLUTELY NO WARRANTY; -This is free software, and you are welcome to redistribute it under certain conditions. -""" - -PROGRESS_DELAY = 0.1 # the progression will be checked every PROGRESS_DELAY s - - -def date_decoder(arg): - return date_utils.date_parse_ext(arg, default_tz=date_utils.TZ_LOCAL) - - -class LiberviaCli: - """ - This class can be use to establish a connection with the - bridge. Moreover, it should manage a main loop. - - To use it, you mainly have to redefine the method run to perform - specify what kind of operation you want to perform. - - """ - def __init__(self): - """ - - @attribute quit_on_progress_end (bool): set to False if you manage yourself - exiting, or if you want the user to stop by himself - @attribute progress_success(callable): method to call when progress just started - by default display a message - @attribute progress_success(callable): method to call when progress is - successfully finished by default display a message - @attribute progress_failure(callable): method to call when progress failed - by default display a message - """ - self.sat_conf = main_config - self.set_color_theme() - bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') - if bridge_module is None: - log.error("Can't import {} bridge".format(bridge_name)) - sys.exit(1) - - self.bridge = bridge_module.AIOBridge() - self._onQuitCallbacks = [] - - def get_config(self, name, section=C.CONFIG_SECTION, default=None): - """Retrieve a setting value from sat.conf""" - return config.config_get(self.sat_conf, section, name, default=default) - - def guess_background(self): - # cf. https://unix.stackexchange.com/a/245568 (thanks!) - try: - # for VTE based terminals - vte_version = int(os.getenv("VTE_VERSION", 0)) - except ValueError: - vte_version = 0 - - color_fg_bg = os.getenv("COLORFGBG") - - if ((sys.stdin.isatty() and sys.stdout.isatty() - and ( - # XTerm - os.getenv("XTERM_VERSION") - # Konsole - or os.getenv("KONSOLE_VERSION") - # All VTE based terminals - or vte_version >= 3502 - ))): - # ANSI escape sequence - stdin_fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(stdin_fd) - try: - tty.setraw(sys.stdin.fileno()) - # we request background color - sys.stdout.write("\033]11;?\a") - sys.stdout.flush() - expected = "\033]11;rgb:" - for c in expected: - ch = sys.stdin.read(1) - if ch != c: - # background id is not supported, we default to "dark" - # TODO: log something? - return 'dark' - red, green, blue = [ - int(c, 16)/65535 for c in sys.stdin.read(14).split('/') - ] - # '\a' is the last character - sys.stdin.read(1) - finally: - termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) - - lum = utils.per_luminance(red, green, blue) - if lum <= 0.5: - return 'dark' - else: - return 'light' - elif color_fg_bg: - # no luck with ANSI escape sequence, we try COLORFGBG environment variable - try: - bg = int(color_fg_bg.split(";")[-1]) - except ValueError: - return "dark" - if bg in list(range(7)) + [8]: - return "dark" - else: - return "light" - else: - # no autodetection method found - return "dark" - - def set_color_theme(self): - background = self.get_config('background', default='auto') - if background == 'auto': - background = self.guess_background() - if background not in ('dark', 'light'): - raise exceptions.ConfigError(_( - 'Invalid value set for "background" ({background}), please check ' - 'your settings in libervia.conf').format( - background=repr(background) - )) - self.background = background - if background == 'light': - C.A_HEADER = A.FG_MAGENTA - C.A_SUBHEADER = A.BOLD + A.FG_RED - C.A_LEVEL_COLORS = (C.A_HEADER, A.BOLD + A.FG_BLUE, A.FG_MAGENTA, A.FG_CYAN) - C.A_SUCCESS = A.FG_GREEN - C.A_FAILURE = A.BOLD + A.FG_RED - C.A_WARNING = A.FG_RED - C.A_PROMPT_PATH = A.FG_BLUE - C.A_PROMPT_SUF = A.BOLD - C.A_DIRECTORY = A.BOLD + A.FG_MAGENTA - C.A_FILE = A.FG_BLACK - - def _bridge_connected(self): - self.parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) - self._make_parents() - self.add_parser_options() - self.subparsers = self.parser.add_subparsers( - title=_('Available commands'), dest='command', required=True) - - # progress attributes - self._progress_id = None # TODO: manage several progress ids - self.quit_on_progress_end = True - - # outputs - self._outputs = {} - for type_ in C.OUTPUT_TYPES: - self._outputs[type_] = OrderedDict() - self.default_output = {} - - self.own_jid = None # must be filled at runtime if needed - - @property - def progress_id(self): - return self._progress_id - - async def set_progress_id(self, progress_id): - # because we use async, we need an explicit setter - self._progress_id = progress_id - await self.replay_cache('progress_ids_cache') - - @property - def watch_progress(self): - try: - self.pbar - except AttributeError: - return False - else: - return True - - @watch_progress.setter - def watch_progress(self, watch_progress): - if watch_progress: - self.pbar = None - - @property - def verbosity(self): - try: - return self.args.verbose - except AttributeError: - return 0 - - async def replay_cache(self, cache_attribute): - """Replay cached signals - - @param cache_attribute(str): name of the attribute containing the cache - if the attribute doesn't exist, there is no cache and the call is ignored - else the cache must be a list of tuples containing the replay callback as - first item, then the arguments to use - """ - try: - cache = getattr(self, cache_attribute) - except AttributeError: - pass - else: - for cache_data in cache: - await cache_data[0](*cache_data[1:]) - - def disp(self, msg, verbosity=0, error=False, end='\n'): - """Print a message to user - - @param msg(unicode): message to print - @param verbosity(int): minimal verbosity to display the message - @param error(bool): if True, print to stderr instead of stdout - @param end(str): string appended after the last value, default a newline - """ - if self.verbosity >= verbosity: - if error: - print(msg, end=end, file=sys.stderr) - else: - print(msg, end=end) - - async def output(self, type_, name, extra_outputs, data): - if name in extra_outputs: - method = extra_outputs[name] - else: - method = self._outputs[type_][name]['callback'] - - ret = method(data) - if inspect.isawaitable(ret): - await ret - - def add_on_quit_callback(self, callback, *args, **kwargs): - """Add a callback which will be called on quit command - - @param callback(callback): method to call - """ - self._onQuitCallbacks.append((callback, args, kwargs)) - - def get_output_choices(self, output_type): - """Return valid output filters for output_type - - @param output_type: True for default, - else can be any registered type - """ - return list(self._outputs[output_type].keys()) - - def _make_parents(self): - self.parents = {} - - # we have a special case here as the start-session option is present only if - # connection is not needed, so we create two similar parents, one with the - # option, the other one without it - for parent_name in ('profile', 'profile_session'): - parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) - parent.add_argument( - "-p", "--profile", action="store", type=str, default='@DEFAULT@', - help=_("Use PROFILE profile key (default: %(default)s)")) - parent.add_argument( - "--pwd", action="store", metavar='PASSWORD', - help=_("Password used to connect profile, if necessary")) - - profile_parent, profile_session_parent = (self.parents['profile'], - self.parents['profile_session']) - - connect_short, connect_long, connect_action, connect_help = ( - "-c", "--connect", "store_true", - _("Connect the profile before doing anything else") - ) - profile_parent.add_argument( - connect_short, connect_long, action=connect_action, help=connect_help) - - profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() - profile_session_connect_group.add_argument( - connect_short, connect_long, action=connect_action, help=connect_help) - profile_session_connect_group.add_argument( - "--start-session", action="store_true", - help=_("Start a profile session without connecting")) - - progress_parent = self.parents['progress'] = argparse.ArgumentParser( - add_help=False) - if progressbar: - progress_parent.add_argument( - "-P", "--progress", action="store_true", help=_("Show progress bar")) - - verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) - verbose_parent.add_argument( - '--verbose', '-v', action='count', default=0, - help=_("Add a verbosity level (can be used multiple times)")) - - quiet_parent = self.parents['quiet'] = argparse.ArgumentParser(add_help=False) - quiet_parent.add_argument( - '--quiet', '-q', action='store_true', - help=_("be quiet (only output machine readable data)")) - - draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) - draft_group = draft_parent.add_argument_group(_('draft handling')) - draft_group.add_argument( - "-D", "--current", action="store_true", help=_("load current draft")) - draft_group.add_argument( - "-F", "--draft-path", type=Path, help=_("path to a draft file to retrieve")) - - - def make_pubsub_group(self, flags, defaults): - """Generate pubsub options according to flags - - @param flags(iterable[unicode]): see [CommandBase.__init__] - @param defaults(dict[unicode, unicode]): help text for default value - key can be "service" or "node" - value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT - @return (ArgumentParser): parser to add - """ - flags = misc.FlagsHandler(flags) - parent = argparse.ArgumentParser(add_help=False) - pubsub_group = parent.add_argument_group('pubsub') - pubsub_group.add_argument("-u", "--pubsub-url", - help=_("Pubsub URL (xmpp or http)")) - - service_help = _("JID of the PubSub service") - if not flags.service: - default = defaults.pop('service', _('PEP service')) - if default is not None: - service_help += _(" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-s", "--service", default='', - help=service_help) - - node_help = _("node to request") - if not flags.node: - default = defaults.pop('node', _('standard node')) - if default is not None: - node_help += _(" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-n", "--node", default='', help=node_help) - - if flags.single_item: - item_help = ("item to retrieve") - if not flags.item: - default = defaults.pop('item', _('last item')) - if default is not None: - item_help += _(" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-i", "--item", default='', - help=item_help) - pubsub_group.add_argument( - "-L", "--last-item", action='store_true', help=_('retrieve last item')) - elif flags.multi_items: - # mutiple items, this activate several features: max-items, RSM, MAM - # and Orbder-by - pubsub_group.add_argument( - "-i", "--item", action='append', dest='items', default=[], - help=_("items to retrieve (DEFAULT: all)")) - if not flags.no_max: - max_group = pubsub_group.add_mutually_exclusive_group() - # XXX: defaut value for --max-items or --max is set in parse_pubsub_args - max_group.add_argument( - "-M", "--max-items", dest="max", type=int, - help=_("maximum number of items to get ({no_limit} to get all items)" - .format(no_limit=C.NO_LIMIT))) - # FIXME: it could be possible to no duplicate max (between pubsub - # max-items and RSM max)should not be duplicated, RSM could be - # used when available and pubsub max otherwise - max_group.add_argument( - "-m", "--max", dest="rsm_max", type=int, - help=_("maximum number of items to get per page (DEFAULT: 10)")) - - # RSM - - rsm_page_group = pubsub_group.add_mutually_exclusive_group() - rsm_page_group.add_argument( - "-a", "--after", dest="rsm_after", - help=_("find page after this item"), metavar='ITEM_ID') - rsm_page_group.add_argument( - "-b", "--before", dest="rsm_before", - help=_("find page before this item"), metavar='ITEM_ID') - rsm_page_group.add_argument( - "--index", dest="rsm_index", type=int, - help=_("index of the first item to retrieve")) - - - # MAM - - pubsub_group.add_argument( - "-f", "--filter", dest='mam_filters', nargs=2, - action='append', default=[], help=_("MAM filters to use"), - metavar=("FILTER_NAME", "VALUE") - ) - - # Order-By - - # TODO: order-by should be a list to handle several levels of ordering - # but this is not yet done in SàT (and not really useful with - # current specifications, as only "creation" and "modification" are - # available) - pubsub_group.add_argument( - "-o", "--order-by", choices=[C.ORDER_BY_CREATION, - C.ORDER_BY_MODIFICATION], - help=_("how items should be ordered")) - - if flags[C.CACHE]: - pubsub_group.add_argument( - "-C", "--no-cache", dest="use_cache", action='store_false', - help=_("don't use Pubsub cache") - ) - - if not flags.all_used: - raise exceptions.InternalError('unknown flags: {flags}'.format( - flags=', '.join(flags.unused))) - if defaults: - raise exceptions.InternalError(f'unused defaults: {defaults}') - - return parent - - def add_parser_options(self): - self.parser.add_argument( - '--version', - action='version', - version=("{name} {version} {copyleft}".format( - name = C.APP_NAME, - version = self.version, - copyleft = COPYLEFT)) - ) - - def register_output(self, type_, name, callback, description="", default=False): - if type_ not in C.OUTPUT_TYPES: - log.error("Invalid output type {}".format(type_)) - return - self._outputs[type_][name] = {'callback': callback, - 'description': description - } - if default: - if type_ in self.default_output: - self.disp( - _('there is already a default output for {type}, ignoring new one') - .format(type=type_) - ) - else: - self.default_output[type_] = name - - - def parse_output_options(self): - options = self.command.args.output_opts - options_dict = {} - for option in options: - try: - key, value = option.split('=', 1) - except ValueError: - key, value = option, None - options_dict[key.strip()] = value.strip() if value is not None else None - return options_dict - - def check_output_options(self, accepted_set, options): - if not accepted_set.issuperset(options): - self.disp( - _("The following output options are invalid: {invalid_options}").format( - invalid_options = ', '.join(set(options).difference(accepted_set))), - error=True) - self.quit(C.EXIT_BAD_ARG) - - def import_plugins(self): - """Automaticaly import commands and outputs in jp - - looks from modules names cmd_*.py in jp path and import them - """ - path = os.path.dirname(sat_frontends.jp.__file__) - # XXX: outputs must be imported before commands as they are used for arguments - for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), - (C.PLUGIN_CMD, 'cmd_*.py')): - modules = ( - os.path.splitext(module)[0] - for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) - for module_name in modules: - module_path = "sat_frontends.jp." + module_name - try: - module = import_module(module_path) - self.import_plugin_module(module, type_) - except ImportError as e: - self.disp( - _("Can't import {module_path} plugin, ignoring it: {e}") - .format(module_path=module_path, e=e), - error=True) - except exceptions.CancelError: - continue - except exceptions.MissingModule as e: - self.disp(_("Missing module for plugin {name}: {missing}".format( - name = module_path, - missing = e)), error=True) - - - def import_plugin_module(self, module, type_): - """add commands or outpus from a module to jp - - @param module: module containing commands or outputs - @param type_(str): one of C_PLUGIN_* - """ - try: - class_names = getattr(module, '__{}__'.format(type_)) - except AttributeError: - log.disp( - _("Invalid plugin module [{type}] {module}") - .format(type=type_, module=module), - error=True) - raise ImportError - else: - for class_name in class_names: - cls = getattr(module, class_name) - cls(self) - - def get_xmpp_uri_from_http(self, http_url): - """parse HTML page at http(s) URL, and looks for xmpp: uri""" - if http_url.startswith('https'): - scheme = 'https' - elif http_url.startswith('http'): - scheme = 'http' - else: - raise exceptions.InternalError('An HTTP scheme is expected in this method') - self.disp(f"{scheme.upper()} URL found, trying to find associated xmpp: URI", 1) - # HTTP URL, we try to find xmpp: links - try: - from lxml import etree - except ImportError: - self.disp( - "lxml module must be installed to use http(s) scheme, please install it " - "with \"pip install lxml\"", - error=True) - self.quit(1) - import urllib.request, urllib.error, urllib.parse - parser = etree.HTMLParser() - try: - root = etree.parse(urllib.request.urlopen(http_url), parser) - except etree.XMLSyntaxError as e: - self.disp(_("Can't parse HTML page : {msg}").format(msg=e)) - links = [] - else: - links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") - if not links: - self.disp( - _('Could not find alternate "xmpp:" URI, can\'t find associated XMPP ' - 'PubSub node/item'), - error=True) - self.quit(1) - xmpp_uri = links[0].get('href') - return xmpp_uri - - def parse_pubsub_args(self): - if self.args.pubsub_url is not None: - url = self.args.pubsub_url - - if url.startswith('http'): - # http(s) URL, we try to retrieve xmpp one from there - url = self.get_xmpp_uri_from_http(url) - - try: - uri_data = uri.parse_xmpp_uri(url) - except ValueError: - self.parser.error(_('invalid XMPP URL: {url}').format(url=url)) - else: - if uri_data['type'] == 'pubsub': - # URL is alright, we only set data not already set by other options - if not self.args.service: - self.args.service = uri_data['path'] - if not self.args.node: - self.args.node = uri_data['node'] - uri_item = uri_data.get('item') - if uri_item: - # there is an item in URI - # we use it only if item is not already set - # and item_last is not used either - try: - item = self.args.item - except AttributeError: - try: - items = self.args.items - except AttributeError: - self.disp( - _("item specified in URL but not needed in command, " - "ignoring it"), - error=True) - else: - if not items: - self.args.items = [uri_item] - else: - if not item: - try: - item_last = self.args.item_last - except AttributeError: - item_last = False - if not item_last: - self.args.item = uri_item - else: - self.parser.error( - _('XMPP URL is not a pubsub one: {url}').format(url=url) - ) - flags = self.args._cmd._pubsub_flags - # we check required arguments here instead of using add_arguments' required option - # because the required argument can be set in URL - if C.SERVICE in flags and not self.args.service: - self.parser.error(_("argument -s/--service is required")) - if C.NODE in flags and not self.args.node: - self.parser.error(_("argument -n/--node is required")) - if C.ITEM in flags and not self.args.item: - self.parser.error(_("argument -i/--item is required")) - - # FIXME: mutually groups can't be nested in a group and don't support title - # so we check conflict here. This may be fixed in Python 3, to be checked - try: - if self.args.item and self.args.item_last: - self.parser.error( - _("--item and --item-last can't be used at the same time")) - except AttributeError: - pass - - try: - max_items = self.args.max - rsm_max = self.args.rsm_max - except AttributeError: - pass - else: - # we need to set a default value for max, but we need to know if we want - # to use pubsub's max or RSM's max. The later is used if any RSM or MAM - # argument is set - if max_items is None and rsm_max is None: - to_check = ('mam_filters', 'rsm_max', 'rsm_after', 'rsm_before', - 'rsm_index') - if any((getattr(self.args, name) for name in to_check)): - # we use RSM - self.args.rsm_max = 10 - else: - # we use pubsub without RSM - self.args.max = 10 - if self.args.max is None: - self.args.max = C.NO_LIMIT - - async def main(self, args, namespace): - try: - await self.bridge.bridge_connect() - except Exception as e: - if isinstance(e, exceptions.BridgeExceptionNoService): - print( - _("Can't connect to Libervia backend, are you sure that it's " - "launched ?") - ) - self.quit(C.EXIT_BACKEND_NOT_FOUND, raise_exc=False) - elif isinstance(e, exceptions.BridgeInitError): - print(_("Can't init bridge")) - self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) - else: - print( - _("Error while initialising bridge: {e}").format(e=e) - ) - self.quit(C.EXIT_BRIDGE_ERROR, raise_exc=False) - return - await self.bridge.ready_get() - self.version = await self.bridge.version_get() - self._bridge_connected() - self.import_plugins() - try: - self.args = self.parser.parse_args(args, namespace=None) - if self.args._cmd._use_pubsub: - self.parse_pubsub_args() - await self.args._cmd.run() - except SystemExit as e: - self.quit(e.code, raise_exc=False) - return - except QuitException: - return - - def _run(self, args=None, namespace=None): - self.loop = JPLoop() - self.loop.run(self, args, namespace) - - @classmethod - def run(cls): - cls()._run() - - def _read_stdin(self, stdin_fut): - """Callback called by ainput to read stdin""" - line = sys.stdin.readline() - if line: - stdin_fut.set_result(line.rstrip(os.linesep)) - else: - stdin_fut.set_exception(EOFError()) - - async def ainput(self, msg=''): - """Asynchronous version of buildin "input" function""" - self.disp(msg, end=' ') - sys.stdout.flush() - loop = asyncio.get_running_loop() - stdin_fut = loop.create_future() - loop.add_reader(sys.stdin, self._read_stdin, stdin_fut) - return await stdin_fut - - async def confirm(self, message): - """Request user to confirm action, return answer as boolean""" - res = await self.ainput(f"{message} (y/N)? ") - return res in ("y", "Y") - - async def confirm_or_quit(self, message, cancel_message=_("action cancelled by user")): - """Request user to confirm action, and quit if he doesn't""" - confirmed = await self.confirm(message) - if not confirmed: - self.disp(cancel_message) - self.quit(C.EXIT_USER_CANCELLED) - - def quit_from_signal(self, exit_code=0): - r"""Same as self.quit, but from a signal handler - - /!\: return must be used after calling this method ! - """ - # XXX: python-dbus will show a traceback if we exit in a signal handler - # so we use this little timeout trick to avoid it - self.loop.call_later(0, self.quit, exit_code) - - def quit(self, exit_code=0, raise_exc=True): - """Terminate the execution with specified exit_code - - This will stop the loop. - @param exit_code(int): code to return when quitting the program - @param raise_exp(boolean): if True raise a QuitException to stop code execution - The default value should be used most of time. - """ - # first the onQuitCallbacks - try: - callbacks_list = self._onQuitCallbacks - except AttributeError: - pass - else: - for callback, args, kwargs in callbacks_list: - callback(*args, **kwargs) - - self.loop.quit(exit_code) - if raise_exc: - raise QuitException - - async def check_jids(self, jids): - """Check jids validity, transform roster name to corresponding jids - - @param profile: profile name - @param jids: list of jids - @return: List of jids - - """ - names2jid = {} - nodes2jid = {} - - try: - contacts = await self.bridge.contacts_get(self.profile) - except BridgeException as e: - if e.classname == "AttributeError": - # we may get an AttributeError if we use a component profile - # as components don't have roster - contacts = [] - else: - raise e - - for contact in contacts: - jid_s, attr, groups = contact - _jid = JID(jid_s) - try: - names2jid[attr["name"].lower()] = jid_s - except KeyError: - pass - - if _jid.node: - nodes2jid[_jid.node.lower()] = jid_s - - def expand_jid(jid): - _jid = jid.lower() - if _jid in names2jid: - expanded = names2jid[_jid] - elif _jid in nodes2jid: - expanded = nodes2jid[_jid] - else: - expanded = jid - return expanded - - def check(jid): - if not jid.is_valid: - log.error (_("%s is not a valid JID !"), jid) - self.quit(1) - - dest_jids=[] - try: - for i in range(len(jids)): - dest_jids.append(expand_jid(jids[i])) - check(dest_jids[i]) - except AttributeError: - pass - - return dest_jids - - async def a_pwd_input(self, msg=''): - """Like ainput but with echo disabled (useful for passwords)""" - # we disable echo, code adapted from getpass standard module which has been - # written by Piers Lauder (original), Guido van Rossum (Windows support and - # cleanup) and Gregory P. Smith (tty support & GetPassWarning), a big thanks - # to them (and for all the amazing work on Python). - stdin_fd = sys.stdin.fileno() - old = termios.tcgetattr(sys.stdin) - new = old[:] - new[3] &= ~termios.ECHO - tcsetattr_flags = termios.TCSAFLUSH - if hasattr(termios, 'TCSASOFT'): - tcsetattr_flags |= termios.TCSASOFT - try: - termios.tcsetattr(stdin_fd, tcsetattr_flags, new) - pwd = await self.ainput(msg=msg) - finally: - termios.tcsetattr(stdin_fd, tcsetattr_flags, old) - sys.stderr.flush() - self.disp('') - return pwd - - async def connect_or_prompt(self, method, err_msg=None): - """Try to connect/start profile session and prompt for password if needed - - @param method(callable): bridge method to either connect or start profile session - It will be called with password as sole argument, use lambda to do the call - properly - @param err_msg(str): message to show if connection fail - """ - password = self.args.pwd - while True: - try: - await method(password or '') - except Exception as e: - if ((isinstance(e, BridgeException) - and e.classname == 'PasswordError' - and self.args.pwd is None)): - if password is not None: - self.disp(A.color(C.A_WARNING, _("invalid password"))) - password = await self.a_pwd_input( - _("please enter profile password:")) - else: - self.disp(err_msg.format(profile=self.profile, e=e), error=True) - self.quit(C.EXIT_ERROR) - else: - break - - async def connect_profile(self): - """Check if the profile is connected and do it if requested - - @exit: - 1 when profile is not connected and --connect is not set - - 1 when the profile doesn't exists - - 1 when there is a connection error - """ - # FIXME: need better exit codes - - self.profile = await self.bridge.profile_name_get(self.args.profile) - - if not self.profile: - log.error( - _("The profile [{profile}] doesn't exist") - .format(profile=self.args.profile) - ) - self.quit(C.EXIT_ERROR) - - try: - start_session = self.args.start_session - except AttributeError: - pass - else: - if start_session: - await self.connect_or_prompt( - lambda pwd: self.bridge.profile_start_session(pwd, self.profile), - err_msg="Can't start {profile}'s session: {e}" - ) - return - elif not await self.bridge.profile_is_session_started(self.profile): - if not self.args.connect: - self.disp(_( - "Session for [{profile}] is not started, please start it " - "before using jp, or use either --start-session or --connect " - "option" - .format(profile=self.profile) - ), error=True) - self.quit(1) - elif not getattr(self.args, "connect", False): - return - - - if not hasattr(self.args, 'connect'): - # a profile can be present without connect option (e.g. on profile - # creation/deletion) - return - elif self.args.connect is True: # if connection is asked, we connect the profile - await self.connect_or_prompt( - lambda pwd: self.bridge.connect(self.profile, pwd, {}), - err_msg = 'Can\'t connect profile "{profile!s}": {e}' - ) - return - else: - if not await self.bridge.is_connected(self.profile): - log.error( - _("Profile [{profile}] is not connected, please connect it " - "before using jp, or use --connect option") - .format(profile=self.profile) - ) - self.quit(1) - - async def get_full_jid(self, param_jid): - """Return the full jid if possible (add main resource when find a bare jid)""" - # TODO: to be removed, bare jid should work with all commands, notably for file - # as backend now handle jingles message initiation - _jid = JID(param_jid) - if not _jid.resource: - #if the resource is not given, we try to add the main resource - main_resource = await self.bridge.main_resource_get(param_jid, self.profile) - if main_resource: - return f"{_jid.bare}/{main_resource}" - return param_jid - - async def get_profile_jid(self): - """Retrieve current profile bare JID if possible""" - full_jid = await self.bridge.param_get_a_async( - "JabberID", "Connection", profile_key=self.profile - ) - return full_jid.rsplit("/", 1)[0] - - -class CommandBase: - - def __init__( - self, - host: LiberviaCli, - name: str, - use_profile: bool = True, - use_output: Union[bool, str] = False, - extra_outputs: Optional[dict] = None, - need_connect: Optional[bool] = None, - help: Optional[str] = None, - **kwargs - ): - """Initialise CommandBase - - @param host: Jp instance - @param name: name of the new command - @param use_profile: if True, add profile selection/connection commands - @param use_output: if not False, add --output option - @param extra_outputs: list of command specific outputs: - key is output name ("default" to use as main output) - value is a callable which will format the output (data will be used as only - argument) - if a key already exists with normal outputs, the extra one will be used - @param need_connect: True if profile connection is needed - False else (profile session must still be started) - None to set auto value (i.e. True if use_profile is set) - Can't be set if use_profile is False - @param help: help message to display - @param **kwargs: args passed to ArgumentParser - use_* are handled directly, they can be: - - use_progress(bool): if True, add progress bar activation option - progress* signals will be handled - - use_verbose(bool): if True, add verbosity option - - use_pubsub(bool): if True, add pubsub options - mandatory arguments are controlled by pubsub_req - - use_draft(bool): if True, add draft handling options - ** other arguments ** - - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, - can be: - C.SERVICE: service is required - C.NODE: node is required - C.ITEM: item is required - C.SINGLE_ITEM: only one item is allowed - """ - try: # If we have subcommands, host is a CommandBase and we need to use host.host - self.host = host.host - except AttributeError: - self.host = host - - # --profile option - parents = kwargs.setdefault('parents', set()) - if use_profile: - # self.host.parents['profile'] is an ArgumentParser with profile connection - # arguments - if need_connect is None: - need_connect = True - parents.add( - self.host.parents['profile' if need_connect else 'profile_session']) - else: - assert need_connect is None - self.need_connect = need_connect - # from this point, self.need_connect is None if connection is not needed at all - # False if session starting is needed, and True if full connection is needed - - # --output option - if use_output: - if extra_outputs is None: - extra_outputs = {} - self.extra_outputs = extra_outputs - if use_output == True: - use_output = C.OUTPUT_TEXT - assert use_output in C.OUTPUT_TYPES - self._output_type = use_output - output_parent = argparse.ArgumentParser(add_help=False) - choices = set(self.host.get_output_choices(use_output)) - choices.update(extra_outputs) - if not choices: - raise exceptions.InternalError( - "No choice found for {} output type".format(use_output)) - try: - default = self.host.default_output[use_output] - except KeyError: - if 'default' in choices: - default = 'default' - elif 'simple' in choices: - default = 'simple' - else: - default = list(choices)[0] - output_parent.add_argument( - '--output', '-O', choices=sorted(choices), default=default, - help=_("select output format (default: {})".format(default))) - output_parent.add_argument( - '--output-option', '--oo', action="append", dest='output_opts', - default=[], help=_("output specific option")) - parents.add(output_parent) - else: - assert extra_outputs is None - - self._use_pubsub = kwargs.pop('use_pubsub', False) - if self._use_pubsub: - flags = kwargs.pop('pubsub_flags', []) - defaults = kwargs.pop('pubsub_defaults', {}) - parents.add(self.host.make_pubsub_group(flags, defaults)) - self._pubsub_flags = flags - - # other common options - use_opts = {k:v for k,v in kwargs.items() if k.startswith('use_')} - for param, do_use in use_opts.items(): - opt=param[4:] # if param is use_verbose, opt is verbose - if opt not in self.host.parents: - raise exceptions.InternalError("Unknown parent option {}".format(opt)) - del kwargs[param] - if do_use: - parents.add(self.host.parents[opt]) - - self.parser = host.subparsers.add_parser(name, help=help, **kwargs) - if hasattr(self, "subcommands"): - self.subparsers = self.parser.add_subparsers(dest='subcommand', required=True) - else: - self.parser.set_defaults(_cmd=self) - self.add_parser_options() - - @property - def sat_conf(self): - return self.host.sat_conf - - @property - def args(self): - return self.host.args - - @property - def profile(self): - return self.host.profile - - @property - def verbosity(self): - return self.host.verbosity - - @property - def progress_id(self): - return self.host.progress_id - - async def set_progress_id(self, progress_id): - return await self.host.set_progress_id(progress_id) - - async def progress_started_handler(self, uid, metadata, profile): - if profile != self.profile: - return - if self.progress_id is None: - # the progress started message can be received before the id - # so we keep progress_started signals in cache to replay they - # when the progress_id is received - cache_data = (self.progress_started_handler, uid, metadata, profile) - try: - cache = self.host.progress_ids_cache - except AttributeError: - cache = self.host.progress_ids_cache = [] - cache.append(cache_data) - else: - if self.host.watch_progress and uid == self.progress_id: - await self.on_progress_started(metadata) - while True: - await asyncio.sleep(PROGRESS_DELAY) - cont = await self.progress_update() - if not cont: - break - - async def progress_finished_handler(self, uid, metadata, profile): - if profile != self.profile: - return - if uid == self.progress_id: - try: - self.host.pbar.finish() - except AttributeError: - pass - await self.on_progress_finished(metadata) - if self.host.quit_on_progress_end: - self.host.quit_from_signal() - - async def progress_error_handler(self, uid, message, profile): - if profile != self.profile: - return - if uid == self.progress_id: - if self.args.progress: - self.disp('') # progress is not finished, so we skip a line - if self.host.quit_on_progress_end: - await self.on_progress_error(message) - self.host.quit_from_signal(C.EXIT_ERROR) - - async def progress_update(self): - """This method is continualy called to update the progress bar - - @return (bool): False to stop being called - """ - data = await self.host.bridge.progress_get(self.progress_id, self.profile) - if data: - try: - size = data['size'] - except KeyError: - self.disp(_("file size is not known, we can't show a progress bar"), 1, - error=True) - return False - if self.host.pbar is None: - #first answer, we must construct the bar - - # if the instance has a pbar_template attribute, it is used has model, - # else default one is used - # template is a list of part, where part can be either a str to show directly - # or a list where first argument is a name of a progressbar widget, and others - # are used as widget arguments - try: - template = self.pbar_template - except AttributeError: - template = [ - _("Progress: "), ["Percentage"], " ", ["Bar"], " ", - ["FileTransferSpeed"], " ", ["ETA"] - ] - - widgets = [] - for part in template: - if isinstance(part, str): - widgets.append(part) - else: - widget = getattr(progressbar, part.pop(0)) - widgets.append(widget(*part)) - - self.host.pbar = progressbar.ProgressBar(max_value=int(size), widgets=widgets) - self.host.pbar.start() - - self.host.pbar.update(int(data['position'])) - - elif self.host.pbar is not None: - return False - - await self.on_progress_update(data) - - return True - - async def on_progress_started(self, metadata): - """Called when progress has just started - - can be overidden by a command - @param metadata(dict): metadata as sent by bridge.progress_started - """ - self.disp(_("Operation started"), 2) - - async def on_progress_update(self, metadata): - """Method called on each progress updata - - can be overidden by a command to handle progress metadata - @para metadata(dict): metadata as returned by bridge.progress_get - """ - pass - - async def on_progress_finished(self, metadata): - """Called when progress has just finished - - can be overidden by a command - @param metadata(dict): metadata as sent by bridge.progress_finished - """ - self.disp(_("Operation successfully finished"), 2) - - async def on_progress_error(self, e): - """Called when a progress failed - - @param error_msg(unicode): error message as sent by bridge.progress_error - """ - self.disp(_("Error while doing operation: {e}").format(e=e), error=True) - - def disp(self, msg, verbosity=0, error=False, end='\n'): - return self.host.disp(msg, verbosity, error, end) - - def output(self, data): - try: - output_type = self._output_type - except AttributeError: - raise exceptions.InternalError( - _('trying to use output when use_output has not been set')) - return self.host.output(output_type, self.args.output, self.extra_outputs, data) - - def get_pubsub_extra(self, extra: Optional[dict] = None) -> str: - """Helper method to compute extra data from pubsub arguments - - @param extra: base extra dict, or None to generate a new one - @return: serialised dict which can be used directly in the bridge for pubsub - """ - if extra is None: - extra = {} - else: - intersection = {C.KEY_ORDER_BY}.intersection(list(extra.keys())) - if intersection: - raise exceptions.ConflictError( - "given extra dict has conflicting keys with pubsub keys " - "{intersection}".format(intersection=intersection)) - - # RSM - - for attribute in ('max', 'after', 'before', 'index'): - key = 'rsm_' + attribute - if key in extra: - raise exceptions.ConflictError( - "This key already exists in extra: u{key}".format(key=key)) - value = getattr(self.args, key, None) - if value is not None: - extra[key] = str(value) - - # MAM - - if hasattr(self.args, 'mam_filters'): - for key, value in self.args.mam_filters: - key = 'filter_' + key - if key in extra: - raise exceptions.ConflictError( - "This key already exists in extra: u{key}".format(key=key)) - extra[key] = value - - # Order-By - - try: - order_by = self.args.order_by - except AttributeError: - pass - else: - if order_by is not None: - extra[C.KEY_ORDER_BY] = self.args.order_by - - # Cache - try: - use_cache = self.args.use_cache - except AttributeError: - pass - else: - if not use_cache: - extra[C.KEY_USE_CACHE] = use_cache - - return data_format.serialise(extra) - - def add_parser_options(self): - try: - subcommands = self.subcommands - except AttributeError: - # We don't have subcommands, the class need to implements add_parser_options - raise NotImplementedError - - # now we add subcommands to ourself - for cls in subcommands: - cls(self) - - def override_pubsub_flags(self, new_flags: Set[str]) -> None: - """Replace pubsub_flags given in __init__ - - useful when a command is extending an other command (e.g. blog command which does - the same as pubsub command, but with a default node) - """ - self._pubsub_flags = new_flags - - async def run(self): - """this method is called when a command is actually run - - It set stuff like progression callbacks and profile connection - You should not overide this method: you should call self.start instead - """ - # we keep a reference to run command, it may be useful e.g. for outputs - self.host.command = self - - try: - show_progress = self.args.progress - except AttributeError: - # the command doesn't use progress bar - pass - else: - if show_progress: - self.host.watch_progress = True - # we need to register the following signal even if we don't display the - # progress bar - self.host.bridge.register_signal( - "progress_started", self.progress_started_handler) - self.host.bridge.register_signal( - "progress_finished", self.progress_finished_handler) - self.host.bridge.register_signal( - "progress_error", self.progress_error_handler) - - if self.need_connect is not None: - await self.host.connect_profile() - await self.start() - - async def start(self): - """This is the starting point of the command, this method must be overriden - - at this point, profile are connected if needed - """ - raise NotImplementedError - - -class CommandAnswering(CommandBase): - """Specialised commands which answer to specific actions - - to manage action_types answer, - """ - action_callbacks = {} # XXX: set managed action types in a dict here: - # key is the action_type, value is the callable - # which will manage the answer. profile filtering is - # already managed when callback is called - - def __init__(self, *args, **kwargs): - super(CommandAnswering, self).__init__(*args, **kwargs) - - async def on_action_new( - self, - action_data_s: str, - action_id: str, - security_limit: int, - profile: str - ) -> None: - if profile != self.profile: - return - action_data = data_format.deserialise(action_data_s) - try: - action_type = action_data['type'] - except KeyError: - try: - xml_ui = action_data["xmlui"] - except KeyError: - pass - else: - self.on_xmlui(xml_ui) - else: - try: - callback = self.action_callbacks[action_type] - except KeyError: - pass - else: - await callback(action_data, action_id, security_limit, profile) - - def on_xmlui(self, xml_ui): - """Display a dialog received from the backend. - - @param xml_ui (unicode): dialog XML representation - """ - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the future - # TODO: XMLUI module - ui = ET.fromstring(xml_ui.encode('utf-8')) - dialog = ui.find("dialog") - if dialog is not None: - self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") - - async def start_answering(self): - """Auto reply to confirmation requests""" - self.host.bridge.register_signal("action_new", self.on_action_new) - actions = await self.host.bridge.actions_get(self.profile) - for action_data_s, action_id, security_limit in actions: - await self.on_action_new(action_data_s, action_id, security_limit, self.profile) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_account.py --- a/sat_frontends/jp/cmd_account.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This module permits to manage XMPP accounts using in-band registration (XEP-0077)""" - -from sat_frontends.jp.constants import Const as C -from sat_frontends.bridge.bridge_frontend import BridgeException -from libervia.backend.core.log import getLogger -from libervia.backend.core.i18n import _ -from sat_frontends.jp import base -from sat_frontends.tools import jid - - -log = getLogger(__name__) - -__commands__ = ["Account"] - - -class AccountCreate(base.CommandBase): - def __init__(self, host): - super(AccountCreate, self).__init__( - host, - "create", - use_profile=False, - use_verbose=True, - help=_("create a XMPP account"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "jid", help=_("jid to create") - ) - self.parser.add_argument( - "password", help=_("password of the account") - ) - self.parser.add_argument( - "-p", - "--profile", - help=_( - "create a profile to use this account (default: don't create profile)" - ), - ) - self.parser.add_argument( - "-e", - "--email", - default="", - help=_("email (usage depends of XMPP server)"), - ) - self.parser.add_argument( - "-H", - "--host", - default="", - help=_("server host (IP address or domain, default: use localhost)"), - ) - self.parser.add_argument( - "-P", - "--port", - type=int, - default=0, - help=_("server port (default: {port})").format( - port=C.XMPP_C2S_PORT - ), - ) - - async def start(self): - try: - await self.host.bridge.in_band_account_new( - self.args.jid, - self.args.password, - self.args.email, - self.args.host, - self.args.port, - ) - - except BridgeException as e: - if e.condition == 'conflict': - self.disp( - f"The account {self.args.jid} already exists", - error=True - ) - self.host.quit(C.EXIT_CONFLICT) - else: - self.disp( - f"can't create account on {self.args.host or 'localhost'!r} with jid " - f"{self.args.jid!r} using In-Band Registration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - - self.disp(_("XMPP account created"), 1) - - if self.args.profile is None: - self.host.quit() - - - self.disp(_("creating profile"), 2) - try: - await self.host.bridge.profile_create( - self.args.profile, - self.args.password, - "", - ) - except BridgeException as e: - if e.condition == 'conflict': - self.disp( - f"The profile {self.args.profile} already exists", - error=True - ) - self.host.quit(C.EXIT_CONFLICT) - else: - self.disp( - _("Can't create profile {profile} to associate with jid " - "{jid}: {e}").format( - profile=self.args.profile, - jid=self.args.jid, - e=e - ), - error=True, - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - - self.disp(_("profile created"), 1) - try: - await self.host.bridge.profile_start_session( - self.args.password, - self.args.profile, - ) - except Exception as e: - self.disp(f"can't start profile session: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - try: - await self.host.bridge.param_set( - "JabberID", - self.args.jid, - "Connection", - profile_key=self.args.profile, - ) - except Exception as e: - self.disp(f"can't set JabberID parameter: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - try: - await self.host.bridge.param_set( - "Password", - self.args.password, - "Connection", - profile_key=self.args.profile, - ) - except Exception as e: - self.disp(f"can't set Password parameter: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.disp( - f"profile {self.args.profile} successfully created and associated to the new " - f"account", 1) - self.host.quit() - - -class AccountModify(base.CommandBase): - def __init__(self, host): - super(AccountModify, self).__init__( - host, "modify", help=_("change password for XMPP account") - ) - - def add_parser_options(self): - self.parser.add_argument( - "password", help=_("new XMPP password") - ) - - async def start(self): - try: - await self.host.bridge.in_band_password_change( - self.args.password, - self.args.profile, - ) - except Exception as e: - self.disp(f"can't change XMPP password: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class AccountDelete(base.CommandBase): - def __init__(self, host): - super(AccountDelete, self).__init__( - host, "delete", help=_("delete a XMPP account") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("delete account without confirmation"), - ) - - async def start(self): - try: - jid_str = await self.host.bridge.param_get_a_async( - "JabberID", - "Connection", - profile_key=self.profile, - ) - except Exception as e: - self.disp(f"can't get JID of the profile: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - jid_ = jid.JID(jid_str) - if not self.args.force: - message = ( - f"You are about to delete the XMPP account with jid {jid_!r}\n" - f"This is the XMPP account of profile {self.profile!r}\n" - f"Are you sure that you want to delete this account?" - ) - await self.host.confirm_or_quit(message, _("Account deletion cancelled")) - - try: - await self.host.bridge.in_band_unregister(jid_.domain, self.args.profile) - except Exception as e: - self.disp(f"can't delete XMPP account with jid {jid_!r}: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.host.quit() - - -class Account(base.CommandBase): - subcommands = (AccountCreate, AccountModify, AccountDelete) - - def __init__(self, host): - super(Account, self).__init__( - host, "account", use_profile=False, help=("XMPP account management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_adhoc.py --- a/sat_frontends/jp/cmd_adhoc.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,203 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import xmlui_manager - -__commands__ = ["AdHoc"] - -FLAG_LOOP = "LOOP" -MAGIC_BAREJID = "@PROFILE_BAREJID@" - - -class Remote(base.CommandBase): - def __init__(self, host): - super(Remote, self).__init__( - host, "remote", use_verbose=True, help=_("remote control a software") - ) - - def add_parser_options(self): - self.parser.add_argument("software", type=str, help=_("software name")) - self.parser.add_argument( - "-j", - "--jids", - nargs="*", - default=[], - help=_("jids allowed to use the command"), - ) - self.parser.add_argument( - "-g", - "--groups", - nargs="*", - default=[], - help=_("groups allowed to use the command"), - ) - self.parser.add_argument( - "--forbidden-groups", - nargs="*", - default=[], - help=_("groups that are *NOT* allowed to use the command"), - ) - self.parser.add_argument( - "--forbidden-jids", - nargs="*", - default=[], - help=_("jids that are *NOT* allowed to use the command"), - ) - self.parser.add_argument( - "-l", "--loop", action="store_true", help=_("loop on the commands") - ) - - async def start(self): - name = self.args.software.lower() - flags = [] - magics = {jid for jid in self.args.jids if jid.count("@") > 1} - magics.add(MAGIC_BAREJID) - jids = set(self.args.jids).difference(magics) - if self.args.loop: - flags.append(FLAG_LOOP) - try: - bus_name, methods = await self.host.bridge.ad_hoc_dbus_add_auto( - name, - list(jids), - self.args.groups, - magics, - self.args.forbidden_jids, - self.args.forbidden_groups, - flags, - self.profile, - ) - except Exception as e: - self.disp(f"can't create remote control: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - if not bus_name: - self.disp(_("No bus name found"), 1) - self.host.quit(C.EXIT_NOT_FOUND) - else: - self.disp(_("Bus name found: [%s]" % bus_name), 1) - for method in methods: - path, iface, command = method - self.disp( - _("Command found: (path:{path}, iface: {iface}) [{command}]") - .format(path=path, iface=iface, command=command), - 1, - ) - self.host.quit() - - -class Run(base.CommandBase): - """Run an Ad-Hoc command""" - - def __init__(self, host): - super(Run, self).__init__( - host, "run", use_verbose=True, help=_("run an Ad-Hoc command") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-j", - "--jid", - default="", - help=_("jid of the service (default: profile's server"), - ) - self.parser.add_argument( - "-S", - "--submit", - action="append_const", - const=xmlui_manager.SUBMIT, - dest="workflow", - help=_("submit form/page"), - ) - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="workflow", - metavar=("KEY", "VALUE"), - help=_("field value"), - ) - self.parser.add_argument( - "node", - nargs="?", - default="", - help=_("node of the command (default: list commands)"), - ) - - async def start(self): - try: - xmlui_raw = await self.host.bridge.ad_hoc_run( - self.args.jid, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get ad-hoc commands list: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - xmlui = xmlui_manager.create(self.host, xmlui_raw) - workflow = self.args.workflow - await xmlui.show(workflow) - if not workflow: - if xmlui.type == "form": - await xmlui.submit_form() - self.host.quit() - - -class List(base.CommandBase): - """List Ad-Hoc commands available on a service""" - - def __init__(self, host): - super(List, self).__init__( - host, "list", use_verbose=True, help=_("list Ad-Hoc commands of a service") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-j", - "--jid", - default="", - help=_("jid of the service (default: profile's server)"), - ) - - async def start(self): - try: - xmlui_raw = await self.host.bridge.ad_hoc_list( - self.args.jid, - self.profile, - ) - except Exception as e: - self.disp(f"can't get ad-hoc commands list: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - xmlui = xmlui_manager.create(self.host, xmlui_raw) - await xmlui.show(read_only=True) - self.host.quit() - - -class AdHoc(base.CommandBase): - subcommands = (Run, List, Remote) - - def __init__(self, host): - super(AdHoc, self).__init__( - host, "ad-hoc", use_profile=False, help=_("Ad-hoc commands") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_application.py --- a/sat_frontends/jp/cmd_application.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from . import base -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp.constants import Const as C - -__commands__ = ["Application"] - - -class List(base.CommandBase): - """List available applications""" - - def __init__(self, host): - super(List, self).__init__( - host, "list", use_profile=False, use_output=C.OUTPUT_LIST, - help=_("list available applications") - ) - - def add_parser_options(self): - # FIXME: "extend" would be better here, but it's only available from Python 3.8+ - # so we use "append" until minimum version of Python is raised. - self.parser.add_argument( - "-f", - "--filter", - dest="filters", - action="append", - choices=["available", "running"], - help=_("show applications with this status"), - ) - - async def start(self): - - # FIXME: this is only needed because we can't use "extend" in - # add_parser_options, see note there - if self.args.filters: - self.args.filters = list(set(self.args.filters)) - else: - self.args.filters = ['available'] - - try: - found_apps = await self.host.bridge.applications_list(self.args.filters) - except Exception as e: - self.disp(f"can't get applications list: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(found_apps) - self.host.quit() - - -class Start(base.CommandBase): - """Start an application""" - - def __init__(self, host): - super(Start, self).__init__( - host, "start", use_profile=False, help=_("start an application") - ) - - def add_parser_options(self): - self.parser.add_argument( - "name", - help=_("name of the application to start"), - ) - - async def start(self): - try: - await self.host.bridge.application_start( - self.args.name, - "", - ) - except Exception as e: - self.disp(f"can't start {self.args.name}: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class Stop(base.CommandBase): - - def __init__(self, host): - super(Stop, self).__init__( - host, "stop", use_profile=False, help=_("stop a running application") - ) - - def add_parser_options(self): - id_group = self.parser.add_mutually_exclusive_group(required=True) - id_group.add_argument( - "name", - nargs="?", - help=_("name of the application to stop"), - ) - id_group.add_argument( - "-i", - "--id", - help=_("identifier of the instance to stop"), - ) - - async def start(self): - try: - if self.args.name is not None: - args = [self.args.name, "name"] - else: - args = [self.args.id, "instance"] - await self.host.bridge.application_stop( - *args, - "", - ) - except Exception as e: - if self.args.name is not None: - self.disp( - f"can't stop application {self.args.name!r}: {e}", error=True) - else: - self.disp( - f"can't stop application instance with id {self.args.id!r}: {e}", - error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class Exposed(base.CommandBase): - - def __init__(self, host): - super(Exposed, self).__init__( - host, "exposed", use_profile=False, use_output=C.OUTPUT_DICT, - help=_("show data exposed by a running application") - ) - - def add_parser_options(self): - id_group = self.parser.add_mutually_exclusive_group(required=True) - id_group.add_argument( - "name", - nargs="?", - help=_("name of the application to check"), - ) - id_group.add_argument( - "-i", - "--id", - help=_("identifier of the instance to check"), - ) - - async def start(self): - try: - if self.args.name is not None: - args = [self.args.name, "name"] - else: - args = [self.args.id, "instance"] - exposed_data_raw = await self.host.bridge.application_exposed_get( - *args, - "", - ) - except Exception as e: - if self.args.name is not None: - self.disp( - f"can't get values exposed from application {self.args.name!r}: {e}", - error=True) - else: - self.disp( - f"can't values exposed from application instance with id {self.args.id!r}: {e}", - error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - exposed_data = data_format.deserialise(exposed_data_raw) - await self.output(exposed_data) - self.host.quit() - - -class Application(base.CommandBase): - subcommands = (List, Start, Stop, Exposed) - - def __init__(self, host): - super(Application, self).__init__( - host, "application", use_profile=False, help=_("manage applications"), - aliases=['app'], - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_avatar.py --- a/sat_frontends/jp/cmd_avatar.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import os -import os.path -import asyncio -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from libervia.backend.tools import config -from libervia.backend.tools.common import data_format - - -__commands__ = ["Avatar"] -DISPLAY_CMD = ["xdg-open", "xv", "display", "gwenview", "showtell"] - - -class Get(base.CommandBase): - def __init__(self, host): - super(Get, self).__init__( - host, "get", use_verbose=True, help=_("retrieve avatar of an entity") - ) - - def add_parser_options(self): - self.parser.add_argument( - "--no-cache", action="store_true", help=_("do no use cached values") - ) - self.parser.add_argument( - "-s", "--show", action="store_true", help=_("show avatar") - ) - self.parser.add_argument("jid", nargs='?', default='', help=_("entity")) - - async def show_image(self, path): - sat_conf = config.parse_main_conf() - cmd = config.config_get(sat_conf, C.CONFIG_SECTION, "image_cmd") - cmds = [cmd] + DISPLAY_CMD if cmd else DISPLAY_CMD - for cmd in cmds: - try: - process = await asyncio.create_subprocess_exec(cmd, path) - ret = await process.wait() - except OSError: - continue - - if ret in (0, 2): - # we can get exit code 2 with display when stopping it with C-c - break - else: - # didn't worked with commands, we try our luck with webbrowser - # in some cases, webbrowser can actually open the associated display program. - # Note that this may be possibly blocking, depending on the platform and - # available browser - import webbrowser - - webbrowser.open(path) - - async def start(self): - try: - avatar_data_raw = await self.host.bridge.avatar_get( - self.args.jid, - not self.args.no_cache, - self.profile, - ) - except Exception as e: - self.disp(f"can't retrieve avatar: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - avatar_data = data_format.deserialise(avatar_data_raw, type_check=None) - - if not avatar_data: - self.disp(_("No avatar found."), 1) - self.host.quit(C.EXIT_NOT_FOUND) - - avatar_path = avatar_data['path'] - - self.disp(avatar_path) - if self.args.show: - await self.show_image(avatar_path) - - self.host.quit() - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__( - host, "set", use_verbose=True, - help=_("set avatar of the profile or an entity") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-j", "--jid", default='', help=_("entity whose avatar must be changed")) - self.parser.add_argument( - "image_path", type=str, help=_("path to the image to upload") - ) - - async def start(self): - path = self.args.image_path - if not os.path.exists(path): - self.disp(_("file {path} doesn't exist!").format(path=repr(path)), error=True) - self.host.quit(C.EXIT_BAD_ARG) - path = os.path.abspath(path) - try: - await self.host.bridge.avatar_set(path, self.args.jid, self.profile) - except Exception as e: - self.disp(f"can't set avatar: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("avatar has been set"), 1) - self.host.quit() - - -class Avatar(base.CommandBase): - subcommands = (Get, Set) - - def __init__(self, host): - super(Avatar, self).__init__( - host, "avatar", use_profile=False, help=_("avatar uploading/retrieving") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_blocking.py --- a/sat_frontends/jp/cmd_blocking.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import json -import os -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C -from . import base - -__commands__ = ["Blocking"] - - -class List(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "list", - use_output=C.OUTPUT_LIST, - help=_("list blocked entities"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - blocked_jids = await self.host.bridge.blocking_list( - self.profile, - ) - except Exception as e: - self.disp(f"can't get blocked entities: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(blocked_jids) - self.host.quit(C.EXIT_OK) - - -class Block(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "block", - help=_("block one or more entities"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "entities", - nargs="+", - metavar="JID", - help=_("JIDs of entities to block"), - ) - - async def start(self): - try: - await self.host.bridge.blocking_block( - self.args.entities, - self.profile - ) - except Exception as e: - self.disp(f"can't block entities: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit(C.EXIT_OK) - - -class Unblock(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "unblock", - help=_("unblock one or more entities"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "entities", - nargs="+", - metavar="JID", - help=_("JIDs of entities to unblock"), - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_('when "all" is used, unblock all entities without confirmation'), - ) - - async def start(self): - if self.args.entities == ["all"]: - if not self.args.force: - await self.host.confirm_or_quit( - _("All entities will be unblocked, are you sure"), - _("unblock cancelled") - ) - self.args.entities.clear() - elif self.args.force: - self.parser.error(_('--force is only allowed when "all" is used as target')) - - try: - await self.host.bridge.blocking_unblock( - self.args.entities, - self.profile - ) - except Exception as e: - self.disp(f"can't unblock entities: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit(C.EXIT_OK) - - -class Blocking(base.CommandBase): - subcommands = (List, Block, Unblock) - - def __init__(self, host): - super().__init__( - host, "blocking", use_profile=False, help=_("entities blocking") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_blog.py --- a/sat_frontends/jp/cmd_blog.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1219 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import asyncio -from asyncio.subprocess import DEVNULL -from configparser import NoOptionError, NoSectionError -import json -import os -import os.path -from pathlib import Path -import re -import subprocess -import sys -import tempfile -from urllib.parse import urlparse - -from libervia.backend.core.i18n import _ -from libervia.backend.tools import config -from libervia.backend.tools.common import uri -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common.ansi import ANSI as A -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C - -from . import base, cmd_pubsub - -__commands__ = ["Blog"] - -SYNTAX_XHTML = "xhtml" -# extensions to use with known syntaxes -SYNTAX_EXT = { - # FIXME: default syntax doesn't sounds needed, there should always be a syntax set - # by the plugin. - "": "txt", # used when the syntax is not found - SYNTAX_XHTML: "xhtml", - "markdown": "md", -} - - -CONF_SYNTAX_EXT = "syntax_ext_dict" -BLOG_TMP_DIR = "blog" -# key to remove from metadata tmp file if they exist -KEY_TO_REMOVE_METADATA = ( - "id", - "content", - "content_xhtml", - "comments_node", - "comments_service", - "updated", -) - -URL_REDIRECT_PREFIX = "url_redirect_" -AIONOTIFY_INSTALL = '"pip install aionotify"' -MB_KEYS = ( - "id", - "url", - "atom_id", - "updated", - "published", - "language", - "comments", # this key is used for all comments* keys - "tags", # this key is used for all tag* keys - "author", - "author_jid", - "author_email", - "author_jid_verified", - "content", - "content_xhtml", - "title", - "title_xhtml", - "extra" -) -OUTPUT_OPT_NO_HEADER = "no-header" -RE_ATTACHMENT_METADATA = re.compile(r"^(?P[a-z_]+)=(?P.*)") -ALLOWER_ATTACH_MD_KEY = ("desc", "media_type", "external") - - -async def guess_syntax_from_path(host, sat_conf, path): - """Return syntax guessed according to filename extension - - @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration - @param path(str): path to the content file - @return(unicode): syntax to use - """ - # we first try to guess syntax with extension - ext = os.path.splitext(path)[1][1:] # we get extension without the '.' - if ext: - for k, v in SYNTAX_EXT.items(): - if k and ext == v: - return k - - # if not found, we use current syntax - return await host.bridge.param_get_a("Syntax", "Composition", "value", host.profile) - - -class BlogPublishCommon: - """handle common option for publising commands (Set and Edit)""" - - async def get_current_syntax(self): - """Retrieve current_syntax - - Use default syntax if --syntax has not been used, else check given syntax. - Will set self.default_syntax_used to True if default syntax has been used - """ - if self.args.syntax is None: - self.default_syntax_used = True - return await self.host.bridge.param_get_a( - "Syntax", "Composition", "value", self.profile - ) - else: - self.default_syntax_used = False - try: - syntax = await self.host.bridge.syntax_get(self.args.syntax) - self.current_syntax = self.args.syntax = syntax - except Exception as e: - if e.classname == "NotFound": - self.parser.error( - _("unknown syntax requested ({syntax})").format( - syntax=self.args.syntax - ) - ) - else: - raise e - return self.args.syntax - - def add_parser_options(self): - self.parser.add_argument("-T", "--title", help=_("title of the item")) - self.parser.add_argument( - "-t", - "--tag", - action="append", - help=_("tag (category) of your item"), - ) - self.parser.add_argument( - "-l", - "--language", - help=_("language of the item (ISO 639 code)"), - ) - - self.parser.add_argument( - "-a", - "--attachment", - dest="attachments", - nargs="+", - help=_( - "attachment in the form URL [metadata_name=value]" - ) - ) - - comments_group = self.parser.add_mutually_exclusive_group() - comments_group.add_argument( - "-C", - "--comments", - action="store_const", - const=True, - dest="comments", - help=_( - "enable comments (default: comments not enabled except if they " - "already exist)" - ), - ) - comments_group.add_argument( - "--no-comments", - action="store_const", - const=False, - dest="comments", - help=_("disable comments (will remove comments node if it exist)"), - ) - - self.parser.add_argument( - "-S", - "--syntax", - help=_("syntax to use (default: get profile's default syntax)"), - ) - self.parser.add_argument( - "-e", - "--encrypt", - action="store_true", - help=_("end-to-end encrypt the blog post") - ) - self.parser.add_argument( - "--encrypt-for", - metavar="JID", - action="append", - help=_("encrypt a single item for") - ) - self.parser.add_argument( - "-X", - "--sign", - action="store_true", - help=_("cryptographically sign the blog post") - ) - - async def set_mb_data_content(self, content, mb_data): - if self.default_syntax_used: - # default syntax has been used - mb_data["content_rich"] = content - elif self.current_syntax == SYNTAX_XHTML: - mb_data["content_xhtml"] = content - else: - mb_data["content_xhtml"] = await self.host.bridge.syntax_convert( - content, self.current_syntax, SYNTAX_XHTML, False, self.profile - ) - - def handle_attachments(self, mb_data: dict) -> None: - """Check, validate and add attachments to mb_data""" - if self.args.attachments: - attachments = [] - attachment = {} - for arg in self.args.attachments: - m = RE_ATTACHMENT_METADATA.match(arg) - if m is None: - # we should have an URL - url_parsed = urlparse(arg) - if url_parsed.scheme not in ("http", "https"): - self.parser.error( - "invalid URL in --attachment (only http(s) scheme is " - f" accepted): {arg}" - ) - if attachment: - # if we hae a new URL, we have a new attachment - attachments.append(attachment) - attachment = {} - attachment["url"] = arg - else: - # we should have a metadata - if "url" not in attachment: - self.parser.error( - "you must to specify an URL before any metadata in " - "--attachment" - ) - key = m.group("key") - if key not in ALLOWER_ATTACH_MD_KEY: - self.parser.error( - f"invalid metadata key in --attachment: {key!r}" - ) - value = m.group("value").strip() - if key == "external": - if not value: - value=True - else: - value = C.bool(value) - attachment[key] = value - if attachment: - attachments.append(attachment) - if attachments: - mb_data.setdefault("extra", {})["attachments"] = attachments - - def set_mb_data_from_args(self, mb_data): - """set microblog metadata according to command line options - - if metadata already exist, it will be overwritten - """ - if self.args.comments is not None: - mb_data["allow_comments"] = self.args.comments - if self.args.tag: - mb_data["tags"] = self.args.tag - if self.args.title is not None: - mb_data["title"] = self.args.title - if self.args.language is not None: - mb_data["language"] = self.args.language - if self.args.encrypt: - mb_data["encrypted"] = True - if self.args.sign: - mb_data["signed"] = True - if self.args.encrypt_for: - mb_data["encrypted_for"] = {"targets": self.args.encrypt_for} - self.handle_attachments(mb_data) - - -class Set(base.CommandBase, BlogPublishCommon): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - help=_("publish a new blog item or update an existing one"), - ) - BlogPublishCommon.__init__(self) - - def add_parser_options(self): - BlogPublishCommon.add_parser_options(self) - - async def start(self): - self.current_syntax = await self.get_current_syntax() - self.pubsub_item = self.args.item - mb_data = {} - self.set_mb_data_from_args(mb_data) - if self.pubsub_item: - mb_data["id"] = self.pubsub_item - content = sys.stdin.read() - await self.set_mb_data_content(content, mb_data) - - try: - item_id = await self.host.bridge.mb_send( - self.args.service, - self.args.node, - data_format.serialise(mb_data), - self.profile, - ) - except Exception as e: - self.disp(f"can't send item: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(f"Item published with ID {item_id}") - self.host.quit(C.EXIT_OK) - - -class Get(base.CommandBase): - TEMPLATE = "blog/articles.html" - - def __init__(self, host): - extra_outputs = {"default": self.default_output, "fancy": self.fancy_output} - base.CommandBase.__init__( - self, - host, - "get", - use_verbose=True, - use_pubsub=True, - pubsub_flags={C.MULTI_ITEMS, C.CACHE}, - use_output=C.OUTPUT_COMPLEX, - extra_outputs=extra_outputs, - help=_("get blog item(s)"), - ) - - def add_parser_options(self): - #  TODO: a key(s) argument to select keys to display - self.parser.add_argument( - "-k", - "--key", - action="append", - dest="keys", - help=_("microblog data key(s) to display (default: depend of verbosity)"), - ) - # TODO: add MAM filters - - def template_data_mapping(self, data): - items, blog_items = data - blog_items["items"] = items - return {"blog_items": blog_items} - - def format_comments(self, item, keys): - lines = [] - for data in item.get("comments", []): - lines.append(data["uri"]) - for k in ("node", "service"): - if OUTPUT_OPT_NO_HEADER in self.args.output_opts: - header = "" - else: - header = f"{C.A_HEADER}comments_{k}: {A.RESET}" - lines.append(header + data[k]) - return "\n".join(lines) - - def format_tags(self, item, keys): - tags = item.pop("tags", []) - return ", ".join(tags) - - def format_updated(self, item, keys): - return common.format_time(item["updated"]) - - def format_published(self, item, keys): - return common.format_time(item["published"]) - - def format_url(self, item, keys): - return uri.build_xmpp_uri( - "pubsub", - subtype="microblog", - path=self.metadata["service"], - node=self.metadata["node"], - item=item["id"], - ) - - def get_keys(self): - """return keys to display according to verbosity or explicit key request""" - verbosity = self.args.verbose - if self.args.keys: - if not set(MB_KEYS).issuperset(self.args.keys): - self.disp( - "following keys are invalid: {invalid}.\n" - "Valid keys are: {valid}.".format( - invalid=", ".join(set(self.args.keys).difference(MB_KEYS)), - valid=", ".join(sorted(MB_KEYS)), - ), - error=True, - ) - self.host.quit(C.EXIT_BAD_ARG) - return self.args.keys - else: - if verbosity == 0: - return ("title", "content") - elif verbosity == 1: - return ( - "title", - "tags", - "author", - "author_jid", - "author_email", - "author_jid_verified", - "published", - "updated", - "content", - ) - else: - return MB_KEYS - - def default_output(self, data): - """simple key/value output""" - items, self.metadata = data - keys = self.get_keys() - - #  k_cb use format_[key] methods for complex formattings - k_cb = {} - for k in keys: - try: - callback = getattr(self, "format_" + k) - except AttributeError: - pass - else: - k_cb[k] = callback - for idx, item in enumerate(items): - for k in keys: - if k not in item and k not in k_cb: - continue - if OUTPUT_OPT_NO_HEADER in self.args.output_opts: - header = "" - else: - header = "{k_fmt}{key}:{k_fmt_e} {sep}".format( - k_fmt=C.A_HEADER, - key=k, - k_fmt_e=A.RESET, - sep="\n" if "content" in k else "", - ) - value = k_cb[k](item, keys) if k in k_cb else item[k] - if isinstance(value, bool): - value = str(value).lower() - elif isinstance(value, dict): - value = repr(value) - self.disp(header + (value or "")) - # we want a separation line after each item but the last one - if idx < len(items) - 1: - print("") - - def fancy_output(self, data): - """display blog is a nice to read way - - this output doesn't use keys filter - """ - # thanks to http://stackoverflow.com/a/943921 - rows, columns = list(map(int, os.popen("stty size", "r").read().split())) - items, metadata = data - verbosity = self.args.verbose - sep = A.color(A.FG_BLUE, columns * "▬") - if items: - print(("\n" + sep + "\n")) - - for idx, item in enumerate(items): - title = item.get("title") - if verbosity > 0: - author = item["author"] - published, updated = item["published"], item.get("updated") - else: - author = published = updated = None - if verbosity > 1: - tags = item.pop("tags", []) - else: - tags = None - content = item.get("content") - - if title: - print((A.color(A.BOLD, A.FG_CYAN, item["title"]))) - meta = [] - if author: - meta.append(A.color(A.FG_YELLOW, author)) - if published: - meta.append(A.color(A.FG_YELLOW, "on ", common.format_time(published))) - if updated != published: - meta.append( - A.color(A.FG_YELLOW, "(updated on ", common.format_time(updated), ")") - ) - print((" ".join(meta))) - if tags: - print((A.color(A.FG_MAGENTA, ", ".join(tags)))) - if (title or tags) and content: - print("") - if content: - self.disp(content) - - print(("\n" + sep + "\n")) - - async def start(self): - try: - mb_data = data_format.deserialise( - await self.host.bridge.mb_get( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - self.get_pubsub_extra(), - self.profile, - ) - ) - except Exception as e: - self.disp(f"can't get blog items: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - items = mb_data.pop("items") - await self.output((items, mb_data)) - self.host.quit(C.EXIT_OK) - - -class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "edit", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - use_draft=True, - use_verbose=True, - help=_("edit an existing or new blog post"), - ) - BlogPublishCommon.__init__(self) - common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) - - def add_parser_options(self): - BlogPublishCommon.add_parser_options(self) - self.parser.add_argument( - "-P", - "--preview", - action="store_true", - help=_("launch a blog preview in parallel"), - ) - self.parser.add_argument( - "--no-publish", - action="store_true", - help=_('add "publish: False" to metadata'), - ) - - def build_metadata_file(self, content_file_path, mb_data=None): - """Build a metadata file using json - - The file is named after content_file_path, with extension replaced by - _metadata.json - @param content_file_path(str): path to the temporary file which will contain the - body - @param mb_data(dict, None): microblog metadata (for existing items) - @return (tuple[dict, Path]): merged metadata put originaly in metadata file - and path to temporary metadata file - """ - # we first construct metadata from edited item ones and CLI argumments - # or re-use the existing one if it exists - meta_file_path = content_file_path.with_name( - content_file_path.stem + common.METADATA_SUFF - ) - if meta_file_path.exists(): - self.disp("Metadata file already exists, we re-use it") - try: - with meta_file_path.open("rb") as f: - mb_data = json.load(f) - except (OSError, IOError, ValueError) as e: - self.disp( - f"Can't read existing metadata file at {meta_file_path}, " - f"aborting: {e}", - error=True, - ) - self.host.quit(1) - else: - mb_data = {} if mb_data is None else mb_data.copy() - - # in all cases, we want to remove unwanted keys - for key in KEY_TO_REMOVE_METADATA: - try: - del mb_data[key] - except KeyError: - pass - # and override metadata with command-line arguments - self.set_mb_data_from_args(mb_data) - - if self.args.no_publish: - mb_data["publish"] = False - - # then we create the file and write metadata there, as JSON dict - # XXX: if we port jp one day on Windows, O_BINARY may need to be added here - with os.fdopen( - os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b" - ) as f: - # we need to use an intermediate unicode buffer to write to the file - # unicode without escaping characters - unicode_dump = json.dumps( - mb_data, - ensure_ascii=False, - indent=4, - separators=(",", ": "), - sort_keys=True, - ) - f.write(unicode_dump.encode("utf-8")) - - return mb_data, meta_file_path - - async def edit(self, content_file_path, content_file_obj, mb_data=None): - """Edit the file contening the content using editor, and publish it""" - # we first create metadata file - meta_ori, meta_file_path = self.build_metadata_file(content_file_path, mb_data) - - coroutines = [] - - # do we need a preview ? - if self.args.preview: - self.disp("Preview requested, launching it", 1) - # we redirect outputs to /dev/null to avoid console pollution in editor - # if user wants to see messages, (s)he can call "blog preview" directly - coroutines.append( - asyncio.create_subprocess_exec( - sys.argv[0], - "blog", - "preview", - "--inotify", - "true", - "-p", - self.profile, - str(content_file_path), - stdout=DEVNULL, - stderr=DEVNULL, - ) - ) - - # we launch editor - coroutines.append( - self.run_editor( - "blog_editor_args", - content_file_path, - content_file_obj, - meta_file_path=meta_file_path, - meta_ori=meta_ori, - ) - ) - - await asyncio.gather(*coroutines) - - async def publish(self, content, mb_data): - await self.set_mb_data_content(content, mb_data) - - if self.pubsub_item: - mb_data["id"] = self.pubsub_item - - mb_data = data_format.serialise(mb_data) - - await self.host.bridge.mb_send( - self.pubsub_service, self.pubsub_node, mb_data, self.profile - ) - self.disp("Blog item published") - - def get_tmp_suff(self): - # we get current syntax to determine file extension - return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[""]) - - async def get_item_data(self, service, node, item): - items = [item] if item else [] - - mb_data = data_format.deserialise( - await self.host.bridge.mb_get( - service, node, 1, items, data_format.serialise({}), self.profile - ) - ) - item = mb_data["items"][0] - - try: - content = item["content_xhtml"] - except KeyError: - content = item["content"] - if content: - content = await self.host.bridge.syntax_convert( - content, "text", SYNTAX_XHTML, False, self.profile - ) - - if content and self.current_syntax != SYNTAX_XHTML: - content = await self.host.bridge.syntax_convert( - content, SYNTAX_XHTML, self.current_syntax, False, self.profile - ) - - if content and self.current_syntax == SYNTAX_XHTML: - content = content.strip() - if not content.startswith("
"): - content = "
" + content + "
" - try: - from lxml import etree - except ImportError: - self.disp(_("You need lxml to edit pretty XHTML")) - else: - parser = etree.XMLParser(remove_blank_text=True) - root = etree.fromstring(content, parser) - content = etree.tostring(root, encoding=str, pretty_print=True) - - return content, item, item["id"] - - async def start(self): - # if there are user defined extension, we use them - SYNTAX_EXT.update( - config.config_get(self.sat_conf, C.CONFIG_SECTION, CONF_SYNTAX_EXT, {}) - ) - self.current_syntax = await self.get_current_syntax() - - ( - self.pubsub_service, - self.pubsub_node, - self.pubsub_item, - content_file_path, - content_file_obj, - mb_data, - ) = await self.get_item_path() - - await self.edit(content_file_path, content_file_obj, mb_data=mb_data) - self.host.quit() - - -class Rename(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "rename", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - help=_("rename an blog item"), - ) - - def add_parser_options(self): - self.parser.add_argument("new_id", help=_("new item id to use")) - - async def start(self): - try: - await self.host.bridge.mb_rename( - self.args.service, - self.args.node, - self.args.item, - self.args.new_id, - self.profile, - ) - except Exception as e: - self.disp(f"can't rename item: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp("Item renamed") - self.host.quit(C.EXIT_OK) - - -class Repeat(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "repeat", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - help=_("repeat (re-publish) a blog item"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - repeat_id = await self.host.bridge.mb_repeat( - self.args.service, - self.args.node, - self.args.item, - "", - self.profile, - ) - except Exception as e: - self.disp(f"can't repeat item: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - if repeat_id: - self.disp(f"Item repeated at ID {str(repeat_id)!r}") - else: - self.disp("Item repeated") - self.host.quit(C.EXIT_OK) - - -class Preview(base.CommandBase, common.BaseEdit): - # TODO: need to be rewritten with template output - - def __init__(self, host): - base.CommandBase.__init__( - self, host, "preview", use_verbose=True, help=_("preview a blog content") - ) - common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) - - def add_parser_options(self): - self.parser.add_argument( - "--inotify", - type=str, - choices=("auto", "true", "false"), - default="auto", - help=_("use inotify to handle preview"), - ) - self.parser.add_argument( - "file", - nargs="?", - default="current", - help=_("path to the content file"), - ) - - async def show_preview(self): - # we implement show_preview here so we don't have to import webbrowser and urllib - # when preview is not used - url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) - self.webbrowser.open_new_tab(url) - - async def _launch_preview_ext(self, cmd_line, opt_name): - url = "file:{}".format(self.urllib.parse.quote(self.preview_file_path)) - args = common.parse_args( - self.host, cmd_line, url=url, preview_file=self.preview_file_path - ) - if not args: - self.disp( - 'Couln\'t find command in "{name}", abording'.format(name=opt_name), - error=True, - ) - self.host.quit(1) - subprocess.Popen(args) - - async def open_preview_ext(self): - await self._launch_preview_ext(self.open_cb_cmd, "blog_preview_open_cmd") - - async def update_preview_ext(self): - await self._launch_preview_ext(self.update_cb_cmd, "blog_preview_update_cmd") - - async def update_content(self): - with self.content_file_path.open("rb") as f: - content = f.read().decode("utf-8-sig") - if content and self.syntax != SYNTAX_XHTML: - # we use safe=True because we want to have a preview as close as possible - # to what the people will see - content = await self.host.bridge.syntax_convert( - content, self.syntax, SYNTAX_XHTML, True, self.profile - ) - - xhtml = ( - f'' - f'' - f"" - f"{content}" - f"" - ) - - with open(self.preview_file_path, "wb") as f: - f.write(xhtml.encode("utf-8")) - - async def start(self): - import webbrowser - import urllib.request, urllib.parse, urllib.error - - self.webbrowser, self.urllib = webbrowser, urllib - - if self.args.inotify != "false": - try: - import aionotify - - except ImportError: - if self.args.inotify == "auto": - aionotify = None - self.disp( - f"aionotify module not found, deactivating feature. You can " - f"install it with {AIONOTIFY_INSTALL}" - ) - else: - self.disp( - f"aioinotify not found, can't activate the feature! Please " - f"install it with {AIONOTIFY_INSTALL}", - error=True, - ) - self.host.quit(1) - else: - aionotify = None - - sat_conf = self.sat_conf - SYNTAX_EXT.update( - config.config_get(sat_conf, C.CONFIG_SECTION, CONF_SYNTAX_EXT, {}) - ) - - try: - self.open_cb_cmd = config.config_get( - sat_conf, C.CONFIG_SECTION, "blog_preview_open_cmd", Exception - ) - except (NoOptionError, NoSectionError): - self.open_cb_cmd = None - open_cb = self.show_preview - else: - open_cb = self.open_preview_ext - - self.update_cb_cmd = config.config_get( - sat_conf, C.CONFIG_SECTION, "blog_preview_update_cmd", self.open_cb_cmd - ) - if self.update_cb_cmd is None: - update_cb = self.show_preview - else: - update_cb = self.update_preview_ext - - # which file do we need to edit? - if self.args.file == "current": - self.content_file_path = self.get_current_file(self.profile) - else: - try: - self.content_file_path = Path(self.args.file).resolve(strict=True) - except FileNotFoundError: - self.disp(_('File "{file}" doesn\'t exist!').format(file=self.args.file)) - self.host.quit(C.EXIT_NOT_FOUND) - - self.syntax = await guess_syntax_from_path( - self.host, sat_conf, self.content_file_path - ) - - # at this point the syntax is converted, we can display the preview - preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False) - self.preview_file_path = preview_file.name - preview_file.close() - await self.update_content() - - if aionotify is None: - # XXX: we don't delete file automatically because browser needs it - # (and webbrowser.open can return before it is read) - self.disp( - f"temporary file created at {self.preview_file_path}\nthis file will NOT " - f"BE DELETED AUTOMATICALLY, please delete it yourself when you have " - f"finished" - ) - await open_cb() - else: - await open_cb() - watcher = aionotify.Watcher() - watcher_kwargs = { - # Watcher don't accept Path so we convert to string - "path": str(self.content_file_path), - "alias": "content_file", - "flags": aionotify.Flags.CLOSE_WRITE - | aionotify.Flags.DELETE_SELF - | aionotify.Flags.MOVE_SELF, - } - watcher.watch(**watcher_kwargs) - - loop = asyncio.get_event_loop() - await watcher.setup(loop) - - try: - while True: - event = await watcher.get_event() - self.disp("Content updated", 1) - if event.flags & ( - aionotify.Flags.DELETE_SELF | aionotify.Flags.MOVE_SELF - ): - self.disp( - "DELETE/MOVE event catched, changing the watch", - 2, - ) - try: - watcher.unwatch("content_file") - except IOError as e: - self.disp( - f"Can't remove the watch: {e}", - 2, - ) - watcher = aionotify.Watcher() - watcher.watch(**watcher_kwargs) - try: - await watcher.setup(loop) - except OSError: - # if the new file is not here yet we can have an error - # as a workaround, we do a little rest and try again - await asyncio.sleep(1) - await watcher.setup(loop) - await self.update_content() - await update_cb() - except FileNotFoundError: - self.disp("The file seems to have been deleted.", error=True) - self.host.quit(C.EXIT_NOT_FOUND) - finally: - os.unlink(self.preview_file_path) - try: - watcher.unwatch("content_file") - except IOError as e: - self.disp( - f"Can't remove the watch: {e}", - 2, - ) - - -class Import(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "import", - use_pubsub=True, - use_progress=True, - help=_("import an external blog"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "importer", - nargs="?", - help=_("importer name, nothing to display importers list"), - ) - self.parser.add_argument("--host", help=_("original blog host")) - self.parser.add_argument( - "--no-images-upload", - action="store_true", - help=_("do *NOT* upload images (default: do upload images)"), - ) - self.parser.add_argument( - "--upload-ignore-host", - help=_("do not upload images from this host (default: upload all images)"), - ) - self.parser.add_argument( - "--ignore-tls-errors", - action="store_true", - help=_("ignore invalide TLS certificate for uploads"), - ) - self.parser.add_argument( - "-o", - "--option", - action="append", - nargs=2, - default=[], - metavar=("NAME", "VALUE"), - help=_("importer specific options (see importer description)"), - ) - self.parser.add_argument( - "location", - nargs="?", - help=_( - "importer data location (see importer description), nothing to show " - "importer description" - ), - ) - - async def on_progress_started(self, metadata): - self.disp(_("Blog upload started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("Blog uploaded successfully"), 2) - redirections = { - k[len(URL_REDIRECT_PREFIX) :]: v - for k, v in metadata.items() - if k.startswith(URL_REDIRECT_PREFIX) - } - if redirections: - conf = "\n".join( - [ - "url_redirections_dict = {}".format( - # we need to add ' ' before each new line - # and to double each '%' for ConfigParser - "\n ".join( - json.dumps(redirections, indent=1, separators=(",", ": ")) - .replace("%", "%%") - .split("\n") - ) - ), - ] - ) - self.disp( - _( - "\nTo redirect old URLs to new ones, put the following lines in your" - " sat.conf file, in [libervia] section:\n\n{conf}" - ).format(conf=conf) - ) - - async def on_progress_error(self, error_msg): - self.disp( - _("Error while uploading blog: {error_msg}").format(error_msg=error_msg), - error=True, - ) - - async def start(self): - if self.args.location is None: - for name in ("option", "service", "no_images_upload"): - if getattr(self.args, name): - self.parser.error( - _( - "{name} argument can't be used without location argument" - ).format(name=name) - ) - if self.args.importer is None: - self.disp( - "\n".join( - [ - f"{name}: {desc}" - for name, desc in await self.host.bridge.blogImportList() - ] - ) - ) - else: - try: - short_desc, long_desc = await self.host.bridge.blogImportDesc( - self.args.importer - ) - except Exception as e: - msg = [l for l in str(e).split("\n") if l][ - -1 - ] # we only keep the last line - self.disp(msg) - self.host.quit(1) - else: - self.disp(f"{self.args.importer}: {short_desc}\n\n{long_desc}") - self.host.quit() - else: - # we have a location, an import is requested - options = {key: value for key, value in self.args.option} - if self.args.host: - options["host"] = self.args.host - if self.args.ignore_tls_errors: - options["ignore_tls_errors"] = C.BOOL_TRUE - if self.args.no_images_upload: - options["upload_images"] = C.BOOL_FALSE - if self.args.upload_ignore_host: - self.parser.error( - "upload-ignore-host option can't be used when no-images-upload " - "is set" - ) - elif self.args.upload_ignore_host: - options["upload_ignore_host"] = self.args.upload_ignore_host - - try: - progress_id = await self.host.bridge.blogImport( - self.args.importer, - self.args.location, - options, - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp( - _("Error while trying to import a blog: {e}").format(e=e), - error=True, - ) - self.host.quit(1) - else: - await self.set_progress_id(progress_id) - - -class AttachmentGet(cmd_pubsub.AttachmentGet): - - def __init__(self, host): - super().__init__(host) - self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM}) - - - async def start(self): - if not self.args.node: - namespaces = await self.host.bridge.namespaces_get() - try: - ns_microblog = namespaces["microblog"] - except KeyError: - self.disp("XEP-0277 plugin is not loaded", error=True) - self.host.quit(C.EXIT_MISSING_FEATURE) - else: - self.args.node = ns_microblog - return await super().start() - - -class AttachmentSet(cmd_pubsub.AttachmentSet): - - def __init__(self, host): - super().__init__(host) - self.override_pubsub_flags({C.SERVICE, C.SINGLE_ITEM}) - - async def start(self): - if not self.args.node: - namespaces = await self.host.bridge.namespaces_get() - try: - ns_microblog = namespaces["microblog"] - except KeyError: - self.disp("XEP-0277 plugin is not loaded", error=True) - self.host.quit(C.EXIT_MISSING_FEATURE) - else: - self.args.node = ns_microblog - return await super().start() - - -class Attachments(base.CommandBase): - subcommands = (AttachmentGet, AttachmentSet) - - def __init__(self, host): - super().__init__( - host, - "attachments", - use_profile=False, - help=_("set or retrieve blog attachments"), - ) - - -class Blog(base.CommandBase): - subcommands = (Set, Get, Edit, Rename, Repeat, Preview, Import, Attachments) - - def __init__(self, host): - super(Blog, self).__init__( - host, "blog", use_profile=False, help=_("blog/microblog management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_bookmarks.py --- a/sat_frontends/jp/cmd_bookmarks.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C - -__commands__ = ["Bookmarks"] - -STORAGE_LOCATIONS = ("local", "private", "pubsub") -TYPES = ("muc", "url") - - -class BookmarksCommon(base.CommandBase): - """Class used to group common options of bookmarks subcommands""" - - def add_parser_options(self, location_default="all"): - self.parser.add_argument( - "-l", - "--location", - type=str, - choices=(location_default,) + STORAGE_LOCATIONS, - default=location_default, - help=_("storage location (default: %(default)s)"), - ) - self.parser.add_argument( - "-t", - "--type", - type=str, - choices=TYPES, - default=TYPES[0], - help=_("bookmarks type (default: %(default)s)"), - ) - - -class BookmarksList(BookmarksCommon): - def __init__(self, host): - super(BookmarksList, self).__init__(host, "list", help=_("list bookmarks")) - - async def start(self): - try: - data = await self.host.bridge.bookmarks_list( - self.args.type, self.args.location, self.host.profile - ) - except Exception as e: - self.disp(f"can't get bookmarks list: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - mess = [] - for location in STORAGE_LOCATIONS: - if not data[location]: - continue - loc_mess = [] - loc_mess.append(f"{location}:") - book_mess = [] - for book_link, book_data in list(data[location].items()): - name = book_data.get("name") - autojoin = book_data.get("autojoin", "false") == "true" - nick = book_data.get("nick") - book_mess.append( - "\t%s[%s%s]%s" - % ( - (name + " ") if name else "", - book_link, - " (%s)" % nick if nick else "", - " (*)" if autojoin else "", - ) - ) - loc_mess.append("\n".join(book_mess)) - mess.append("\n".join(loc_mess)) - - print("\n\n".join(mess)) - self.host.quit() - - -class BookmarksRemove(BookmarksCommon): - def __init__(self, host): - super(BookmarksRemove, self).__init__(host, "remove", help=_("remove a bookmark")) - - def add_parser_options(self): - super(BookmarksRemove, self).add_parser_options() - self.parser.add_argument( - "bookmark", help=_("jid (for muc bookmark) or url of to remove") - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("delete bookmark without confirmation"), - ) - - async def start(self): - if not self.args.force: - await self.host.confirm_or_quit(_("Are you sure to delete this bookmark?")) - - try: - await self.host.bridge.bookmarks_remove( - self.args.type, self.args.bookmark, self.args.location, self.host.profile - ) - except Exception as e: - self.disp(_("can't delete bookmark: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("bookmark deleted")) - self.host.quit() - - -class BookmarksAdd(BookmarksCommon): - def __init__(self, host): - super(BookmarksAdd, self).__init__(host, "add", help=_("add a bookmark")) - - def add_parser_options(self): - super(BookmarksAdd, self).add_parser_options(location_default="auto") - self.parser.add_argument( - "bookmark", help=_("jid (for muc bookmark) or url of to remove") - ) - self.parser.add_argument("-n", "--name", help=_("bookmark name")) - muc_group = self.parser.add_argument_group(_("MUC specific options")) - muc_group.add_argument("-N", "--nick", help=_("nickname")) - muc_group.add_argument( - "-a", - "--autojoin", - action="store_true", - help=_("join room on profile connection"), - ) - - async def start(self): - if self.args.type == "url" and (self.args.autojoin or self.args.nick is not None): - self.parser.error(_("You can't use --autojoin or --nick with --type url")) - data = {} - if self.args.autojoin: - data["autojoin"] = "true" - if self.args.nick is not None: - data["nick"] = self.args.nick - if self.args.name is not None: - data["name"] = self.args.name - try: - await self.host.bridge.bookmarks_add( - self.args.type, - self.args.bookmark, - data, - self.args.location, - self.host.profile, - ) - except Exception as e: - self.disp(f"can't add bookmark: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("bookmark successfully added")) - self.host.quit() - - -class Bookmarks(base.CommandBase): - subcommands = (BookmarksList, BookmarksRemove, BookmarksAdd) - - def __init__(self, host): - super(Bookmarks, self).__init__( - host, "bookmarks", use_profile=False, help=_("manage bookmarks") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_debug.py --- a/sat_frontends/jp/cmd_debug.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from libervia.backend.tools.common.ansi import ANSI as A -import json - -__commands__ = ["Debug"] - - -class BridgeCommon(object): - def eval_args(self): - if self.args.arg: - try: - return eval("[{}]".format(",".join(self.args.arg))) - except SyntaxError as e: - self.disp( - "Can't evaluate arguments: {mess}\n{text}\n{offset}^".format( - mess=e, text=e.text, offset=" " * (e.offset - 1) - ), - error=True, - ) - self.host.quit(C.EXIT_BAD_ARG) - else: - return [] - - -class Method(base.CommandBase, BridgeCommon): - def __init__(self, host): - base.CommandBase.__init__(self, host, "method", help=_("call a bridge method")) - BridgeCommon.__init__(self) - - def add_parser_options(self): - self.parser.add_argument( - "method", type=str, help=_("name of the method to execute") - ) - self.parser.add_argument("arg", nargs="*", help=_("argument of the method")) - - async def start(self): - method = getattr(self.host.bridge, self.args.method) - import inspect - - argspec = inspect.getargspec(method) - - kwargs = {} - if "profile_key" in argspec.args: - kwargs["profile_key"] = self.profile - elif "profile" in argspec.args: - kwargs["profile"] = self.profile - - args = self.eval_args() - - try: - ret = await method( - *args, - **kwargs, - ) - except Exception as e: - self.disp( - _("Error while executing {method}: {e}").format( - method=self.args.method, e=e - ), - error=True, - ) - self.host.quit(C.EXIT_ERROR) - else: - if ret is not None: - self.disp(str(ret)) - self.host.quit() - - -class Signal(base.CommandBase, BridgeCommon): - def __init__(self, host): - base.CommandBase.__init__( - self, host, "signal", help=_("send a fake signal from backend") - ) - BridgeCommon.__init__(self) - - def add_parser_options(self): - self.parser.add_argument("signal", type=str, help=_("name of the signal to send")) - self.parser.add_argument("arg", nargs="*", help=_("argument of the signal")) - - async def start(self): - args = self.eval_args() - json_args = json.dumps(args) - # XXX: we use self.args.profile and not self.profile - # because we want the raw profile_key (so plugin handle C.PROF_KEY_NONE) - try: - await self.host.bridge.debug_signal_fake( - self.args.signal, json_args, self.args.profile - ) - except Exception as e: - self.disp(_("Can't send fake signal: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_ERROR) - else: - self.host.quit() - - -class bridge(base.CommandBase): - subcommands = (Method, Signal) - - def __init__(self, host): - super(bridge, self).__init__( - host, "bridge", use_profile=False, help=_("bridge s(t)imulation") - ) - - -class Monitor(base.CommandBase): - def __init__(self, host): - super(Monitor, self).__init__( - host, - "monitor", - use_verbose=True, - use_profile=False, - use_output=C.OUTPUT_XML, - help=_("monitor XML stream"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-d", - "--direction", - choices=("in", "out", "both"), - default="both", - help=_("stream direction filter"), - ) - - async def print_xml(self, direction, xml_data, profile): - if self.args.direction == "in" and direction != "IN": - return - if self.args.direction == "out" and direction != "OUT": - return - verbosity = self.host.verbosity - if not xml_data.strip(): - if verbosity <= 2: - return - whiteping = True - else: - whiteping = False - - if verbosity: - profile_disp = f" ({profile})" if verbosity > 1 else "" - if direction == "IN": - self.disp( - A.color( - A.BOLD, A.FG_YELLOW, "<<<===== IN ====", A.FG_WHITE, profile_disp - ) - ) - else: - self.disp( - A.color( - A.BOLD, A.FG_CYAN, "==== OUT ====>>>", A.FG_WHITE, profile_disp - ) - ) - if whiteping: - self.disp("[WHITESPACE PING]") - else: - try: - await self.output(xml_data) - except Exception: - #  initial stream is not valid XML, - # in this case we print directly to data - #  FIXME: we should test directly lxml.etree.XMLSyntaxError - # but importing lxml directly here is not clean - # should be wrapped in a custom Exception - self.disp(xml_data) - self.disp("") - - async def start(self): - self.host.bridge.register_signal("xml_log", self.print_xml, "plugin") - - -class Theme(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, host, "theme", help=_("print colours used with your background") - ) - - def add_parser_options(self): - pass - - async def start(self): - print(f"background currently used: {A.BOLD}{self.host.background}{A.RESET}\n") - for attr in dir(C): - if not attr.startswith("A_"): - continue - color = getattr(C, attr) - if attr == "A_LEVEL_COLORS": - # This constant contains multiple colors - self.disp("LEVEL COLORS: ", end=" ") - for idx, c in enumerate(color): - last = idx == len(color) - 1 - end = "\n" if last else " " - self.disp( - c + f"LEVEL_{idx}" + A.RESET + (", " if not last else ""), end=end - ) - else: - text = attr[2:] - self.disp(A.color(color, text)) - self.host.quit() - - -class Debug(base.CommandBase): - subcommands = (bridge, Monitor, Theme) - - def __init__(self, host): - super(Debug, self).__init__( - host, "debug", use_profile=False, help=_("debugging tools") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_encryption.py --- a/sat_frontends/jp/cmd_encryption.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,231 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from sat_frontends.jp import base -from sat_frontends.jp.constants import Const as C -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp import xmlui_manager - -__commands__ = ["Encryption"] - - -class EncryptionAlgorithms(base.CommandBase): - - def __init__(self, host): - extra_outputs = {"default": self.default_output} - super(EncryptionAlgorithms, self).__init__( - host, "algorithms", - use_output=C.OUTPUT_LIST_DICT, - extra_outputs=extra_outputs, - use_profile=False, - help=_("show available encryption algorithms")) - - def add_parser_options(self): - pass - - def default_output(self, plugins): - if not plugins: - self.disp(_("No encryption plugin registered!")) - else: - self.disp(_("Following encryption algorithms are available: {algos}").format( - algos=', '.join([p['name'] for p in plugins]))) - - async def start(self): - try: - plugins_ser = await self.host.bridge.encryption_plugins_get() - plugins = data_format.deserialise(plugins_ser, type_check=list) - except Exception as e: - self.disp(f"can't retrieve plugins: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(plugins) - self.host.quit() - - -class EncryptionGet(base.CommandBase): - - def __init__(self, host): - super(EncryptionGet, self).__init__( - host, "get", - use_output=C.OUTPUT_DICT, - help=_("get encryption session data")) - - def add_parser_options(self): - self.parser.add_argument( - "jid", - help=_("jid of the entity to check") - ) - - async def start(self): - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - try: - serialised = await self.host.bridge.message_encryption_get(jid, self.profile) - except Exception as e: - self.disp(f"can't get session: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - session_data = data_format.deserialise(serialised) - if session_data is None: - self.disp( - "No encryption session found, the messages are sent in plain text.") - self.host.quit(C.EXIT_NOT_FOUND) - await self.output(session_data) - self.host.quit() - - -class EncryptionStart(base.CommandBase): - - def __init__(self, host): - super(EncryptionStart, self).__init__( - host, "start", - help=_("start encrypted session with an entity")) - - def add_parser_options(self): - self.parser.add_argument( - "--encrypt-noreplace", - action="store_true", - help=_("don't replace encryption algorithm if an other one is already used")) - algorithm = self.parser.add_mutually_exclusive_group() - algorithm.add_argument( - "-n", "--name", help=_("algorithm name (DEFAULT: choose automatically)")) - algorithm.add_argument( - "-N", "--namespace", - help=_("algorithm namespace (DEFAULT: choose automatically)")) - self.parser.add_argument( - "jid", - help=_("jid of the entity to stop encrypted session with") - ) - - async def start(self): - if self.args.name is not None: - try: - namespace = await self.host.bridge.encryption_namespace_get(self.args.name) - except Exception as e: - self.disp(f"can't get encryption namespace: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - elif self.args.namespace is not None: - namespace = self.args.namespace - else: - namespace = "" - - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - - try: - await self.host.bridge.message_encryption_start( - jid, namespace, not self.args.encrypt_noreplace, - self.profile) - except Exception as e: - self.disp(f"can't get encryption namespace: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.host.quit() - - -class EncryptionStop(base.CommandBase): - - def __init__(self, host): - super(EncryptionStop, self).__init__( - host, "stop", - help=_("stop encrypted session with an entity")) - - def add_parser_options(self): - self.parser.add_argument( - "jid", - help=_("jid of the entity to stop encrypted session with") - ) - - async def start(self): - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - try: - await self.host.bridge.message_encryption_stop(jid, self.profile) - except Exception as e: - self.disp(f"can't end encrypted session: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.host.quit() - - -class TrustUI(base.CommandBase): - - def __init__(self, host): - super(TrustUI, self).__init__( - host, "ui", - help=_("get UI to manage trust")) - - def add_parser_options(self): - self.parser.add_argument( - "jid", - help=_("jid of the entity to stop encrypted session with") - ) - algorithm = self.parser.add_mutually_exclusive_group() - algorithm.add_argument( - "-n", "--name", help=_("algorithm name (DEFAULT: current algorithm)")) - algorithm.add_argument( - "-N", "--namespace", - help=_("algorithm namespace (DEFAULT: current algorithm)")) - - async def start(self): - if self.args.name is not None: - try: - namespace = await self.host.bridge.encryption_namespace_get(self.args.name) - except Exception as e: - self.disp(f"can't get encryption namespace: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - elif self.args.namespace is not None: - namespace = self.args.namespace - else: - namespace = "" - - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - - try: - xmlui_raw = await self.host.bridge.encryption_trust_ui_get( - jid, namespace, self.profile) - except Exception as e: - self.disp(f"can't get encryption session trust UI: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - xmlui = xmlui_manager.create(self.host, xmlui_raw) - await xmlui.show() - if xmlui.type != C.XMLUI_DIALOG: - await xmlui.submit_form() - self.host.quit() - -class EncryptionTrust(base.CommandBase): - subcommands = (TrustUI,) - - def __init__(self, host): - super(EncryptionTrust, self).__init__( - host, "trust", use_profile=False, help=_("trust manangement") - ) - - -class Encryption(base.CommandBase): - subcommands = (EncryptionAlgorithms, EncryptionGet, EncryptionStart, EncryptionStop, - EncryptionTrust) - - def __init__(self, host): - super(Encryption, self).__init__( - host, "encryption", use_profile=False, help=_("encryption sessions handling") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_event.py --- a/sat_frontends/jp/cmd_event.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,755 +0,0 @@ -#!/usr/bin/env python3 - - -# libervia-cli: Libervia CLI frontend -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import argparse -import sys - -from sqlalchemy import desc - -from libervia.backend.core.i18n import _ -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common import date_utils -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.tools.common.ansi import ANSI as A -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp.constants import Const as C - -from . import base - -__commands__ = ["Event"] - -OUTPUT_OPT_TABLE = "table" - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_LIST_DICT, - use_pubsub=True, - pubsub_flags={C.MULTI_ITEMS, C.CACHE}, - use_verbose=True, - extra_outputs={ - "default": self.default_output, - }, - help=_("get event(s) data"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - events_data_s = await self.host.bridge.events_get( - self.args.service, - self.args.node, - self.args.items, - self.get_pubsub_extra(), - self.profile, - ) - except Exception as e: - self.disp(f"can't get events data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - events_data = data_format.deserialise(events_data_s, type_check=list) - await self.output(events_data) - self.host.quit() - - def default_output(self, events): - nb_events = len(events) - for idx, event in enumerate(events): - names = event["name"] - name = names.get("") or next(iter(names.values())) - start = event["start"] - start_human = date_utils.date_fmt( - start, "medium", tz_info=date_utils.TZ_LOCAL - ) - end = event["end"] - self.disp(A.color( - A.BOLD, start_human, A.RESET, " ", - f"({date_utils.delta2human(start, end)}) ", - C.A_HEADER, name - )) - if self.verbosity > 0: - descriptions = event.get("descriptions", []) - if descriptions: - self.disp(descriptions[0]["description"]) - if idx < (nb_events-1): - self.disp("") - - -class CategoryAction(argparse.Action): - - def __init__(self, option_strings, dest, nargs=None, metavar=None, **kwargs): - if nargs is not None or metavar is not None: - raise ValueError("nargs and metavar must not be used") - if metavar is not None: - metavar="TERM WIKIDATA_ID LANG" - if "--help" in sys.argv: - # FIXME: dirty workaround to have correct --help message - # argparse doesn't normally allow variable number of arguments beside "+" - # and "*", this workaround show METAVAR as 3 arguments were expected, while - # we can actuall use 1, 2 or 3. - nargs = 3 - metavar = ("TERM", "[WIKIDATA_ID]", "[LANG]") - else: - nargs = "+" - - super().__init__(option_strings, dest, metavar=metavar, nargs=nargs, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - categories = getattr(namespace, self.dest) - if categories is None: - categories = [] - setattr(namespace, self.dest, categories) - - if not values: - parser.error("category values must be set") - - category = { - "term": values[0] - } - - if len(values) == 1: - pass - elif len(values) == 2: - value = values[1] - if value.startswith("Q"): - category["wikidata_id"] = value - else: - category["language"] = value - elif len(values) == 3: - __, wd, lang = values - category["wikidata_id"] = wd - category["language"] = lang - else: - parser.error("Category can't have more than 3 arguments") - - categories.append(category) - - -class EventBase: - def add_parser_options(self): - self.parser.add_argument( - "-S", "--start", type=base.date_decoder, metavar="TIME_PATTERN", - help=_("the start time of the event")) - end_group = self.parser.add_mutually_exclusive_group() - end_group.add_argument( - "-E", "--end", type=base.date_decoder, metavar="TIME_PATTERN", - help=_("the time of the end of the event")) - end_group.add_argument( - "-D", "--duration", help=_("duration of the event")) - self.parser.add_argument( - "-H", "--head-picture", help="URL to a picture to use as head-picture" - ) - self.parser.add_argument( - "-d", "--description", help="plain text description the event" - ) - self.parser.add_argument( - "-C", "--category", action=CategoryAction, dest="categories", - help="Category of the event" - ) - self.parser.add_argument( - "-l", "--location", action="append", nargs="+", metavar="[KEY] VALUE", - help="Location metadata" - ) - rsvp_group = self.parser.add_mutually_exclusive_group() - rsvp_group.add_argument( - "--rsvp", action="store_true", help=_("RSVP is requested")) - rsvp_group.add_argument( - "--rsvp_json", metavar="JSON", help=_("JSON description of RSVP form")) - for node_type in ("invitees", "comments", "blog", "schedule"): - self.parser.add_argument( - f"--{node_type}", - nargs=2, - metavar=("JID", "NODE"), - help=_("link {node_type} pubsub node").format(node_type=node_type) - ) - self.parser.add_argument( - "-a", "--attachment", action="append", dest="attachments", - help=_("attach a file") - ) - self.parser.add_argument("--website", help=_("website of the event")) - self.parser.add_argument( - "--status", choices=["confirmed", "tentative", "cancelled"], - help=_("status of the event") - ) - self.parser.add_argument( - "-T", "--language", metavar="LANG", action="append", dest="languages", - help=_("main languages spoken at the event") - ) - self.parser.add_argument( - "--wheelchair", choices=["full", "partial", "no"], - help=_("is the location accessible by wheelchair") - ) - self.parser.add_argument( - "--external", - nargs=3, - metavar=("JID", "NODE", "ITEM"), - help=_("link to an external event") - ) - - def get_event_data(self): - if self.args.duration is not None: - if self.args.start is None: - self.parser.error("--start must be send if --duration is used") - # if duration is used, we simply add it to start time to get end time - self.args.end = base.date_decoder(f"{self.args.start} + {self.args.duration}") - - event = {} - if self.args.name is not None: - event["name"] = {"": self.args.name} - - if self.args.start is not None: - event["start"] = self.args.start - - if self.args.end is not None: - event["end"] = self.args.end - - if self.args.head_picture: - event["head-picture"] = { - "sources": [{ - "url": self.args.head_picture - }] - } - if self.args.description: - event["descriptions"] = [ - { - "type": "text", - "description": self.args.description - } - ] - if self.args.categories: - event["categories"] = self.args.categories - if self.args.location is not None: - location = {} - for location_data in self.args.location: - if len(location_data) == 1: - location["description"] = location_data[0] - else: - key, *values = location_data - location[key] = " ".join(values) - event["locations"] = [location] - - if self.args.rsvp: - event["rsvp"] = [{}] - elif self.args.rsvp_json: - if isinstance(self.args.rsvp_elt, dict): - event["rsvp"] = [self.args.rsvp_json] - else: - event["rsvp"] = self.args.rsvp_json - - for node_type in ("invitees", "comments", "blog", "schedule"): - value = getattr(self.args, node_type) - if value: - service, node = value - event[node_type] = {"service": service, "node": node} - - if self.args.attachments: - attachments = event["attachments"] = [] - for attachment in self.args.attachments: - attachments.append({ - "sources": [{"url": attachment}] - }) - - extra = {} - - for arg in ("website", "status", "languages"): - value = getattr(self.args, arg) - if value is not None: - extra[arg] = value - if self.args.wheelchair is not None: - extra["accessibility"] = {"wheelchair": self.args.wheelchair} - - if extra: - event["extra"] = extra - - if self.args.external: - ext_jid, ext_node, ext_item = self.args.external - event["external"] = { - "jid": ext_jid, - "node": ext_node, - "item": ext_item - } - return event - - -class Create(EventBase, base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "create", - use_pubsub=True, - help=_("create or replace event"), - ) - - def add_parser_options(self): - super().add_parser_options() - self.parser.add_argument( - "-i", - "--id", - default="", - help=_("ID of the PubSub Item"), - ) - # name is mandatory here - self.parser.add_argument("name", help=_("name of the event")) - - async def start(self): - if self.args.start is None: - self.parser.error("--start must be set") - event_data = self.get_event_data() - # we check self.args.end after get_event_data because it may be set there id - # --duration is used - if self.args.end is None: - self.parser.error("--end or --duration must be set") - try: - await self.host.bridge.event_create( - data_format.serialise(event_data), - self.args.id, - self.args.node, - self.args.service, - self.profile, - ) - except Exception as e: - self.disp(f"can't create event: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("Event created successfuly)")) - self.host.quit() - - -class Modify(EventBase, base.CommandBase): - def __init__(self, host): - super(Modify, self).__init__( - host, - "modify", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - help=_("modify an existing event"), - ) - EventBase.__init__(self) - - def add_parser_options(self): - super().add_parser_options() - # name is optional here - self.parser.add_argument("-N", "--name", help=_("name of the event")) - - async def start(self): - event_data = self.get_event_data() - try: - await self.host.bridge.event_modify( - data_format.serialise(event_data), - self.args.item, - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't update event data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class InviteeGet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - use_verbose=True, - help=_("get event attendance"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-j", "--jid", action="append", dest="jids", default=[], - help=_("only retrieve RSVP from those JIDs") - ) - - async def start(self): - try: - event_data_s = await self.host.bridge.event_invitee_get( - self.args.service, - self.args.node, - self.args.item, - self.args.jids, - "", - self.profile, - ) - except Exception as e: - self.disp(f"can't get event data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - event_data = data_format.deserialise(event_data_s) - await self.output(event_data) - self.host.quit() - - -class InviteeSet(base.CommandBase): - def __init__(self, host): - super(InviteeSet, self).__init__( - host, - "set", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM}, - help=_("set event attendance"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="fields", - metavar=("KEY", "VALUE"), - help=_("configuration field to set"), - ) - - async def start(self): - # TODO: handle RSVP with XMLUI in a similar way as for `ad-hoc run` - fields = dict(self.args.fields) if self.args.fields else {} - try: - self.host.bridge.event_invitee_set( - self.args.service, - self.args.node, - self.args.item, - data_format.serialise(fields), - self.profile, - ) - except Exception as e: - self.disp(f"can't set event data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class InviteesList(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - base.CommandBase.__init__( - self, - host, - "list", - use_output=C.OUTPUT_DICT_DICT, - extra_outputs=extra_outputs, - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("get event attendance"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-m", - "--missing", - action="store_true", - help=_("show missing people (invited but no R.S.V.P. so far)"), - ) - self.parser.add_argument( - "-R", - "--no-rsvp", - action="store_true", - help=_("don't show people which gave R.S.V.P."), - ) - - def _attend_filter(self, attend, row): - if attend == "yes": - attend_color = C.A_SUCCESS - elif attend == "no": - attend_color = C.A_FAILURE - else: - attend_color = A.FG_WHITE - return A.color(attend_color, attend) - - def _guests_filter(self, guests): - return "(" + str(guests) + ")" if guests else "" - - def default_output(self, event_data): - data = [] - attendees_yes = 0 - attendees_maybe = 0 - attendees_no = 0 - attendees_missing = 0 - guests = 0 - guests_maybe = 0 - for jid_, jid_data in event_data.items(): - jid_data["jid"] = jid_ - try: - guests_int = int(jid_data["guests"]) - except (ValueError, KeyError): - pass - attend = jid_data.get("attend", "") - if attend == "yes": - attendees_yes += 1 - guests += guests_int - elif attend == "maybe": - attendees_maybe += 1 - guests_maybe += guests_int - elif attend == "no": - attendees_no += 1 - jid_data["guests"] = "" - else: - attendees_missing += 1 - jid_data["guests"] = "" - data.append(jid_data) - - show_table = OUTPUT_OPT_TABLE in self.args.output_opts - - table = common.Table.from_list_dict( - self.host, - data, - ("nick",) + (("jid",) if self.host.verbosity else ()) + ("attend", "guests"), - headers=None, - filters={ - "nick": A.color(C.A_HEADER, "{}" if show_table else "{} "), - "jid": "{}" if show_table else "{} ", - "attend": self._attend_filter, - "guests": "{}" if show_table else self._guests_filter, - }, - defaults={"nick": "", "attend": "", "guests": 1}, - ) - if show_table: - table.display() - else: - table.display_blank(show_header=False, col_sep="") - - if not self.args.no_rsvp: - self.disp("") - self.disp( - A.color( - C.A_SUBHEADER, - _("Attendees: "), - A.RESET, - str(len(data)), - _(" ("), - C.A_SUCCESS, - _("yes: "), - str(attendees_yes), - A.FG_WHITE, - _(", maybe: "), - str(attendees_maybe), - ", ", - C.A_FAILURE, - _("no: "), - str(attendees_no), - A.RESET, - ")", - ) - ) - self.disp( - A.color(C.A_SUBHEADER, _("confirmed guests: "), A.RESET, str(guests)) - ) - self.disp( - A.color( - C.A_SUBHEADER, - _("unconfirmed guests: "), - A.RESET, - str(guests_maybe), - ) - ) - self.disp( - A.color(C.A_SUBHEADER, _("total: "), A.RESET, str(guests + guests_maybe)) - ) - if attendees_missing: - self.disp("") - self.disp( - A.color( - C.A_SUBHEADER, - _("missing people (no reply): "), - A.RESET, - str(attendees_missing), - ) - ) - - async def start(self): - if self.args.no_rsvp and not self.args.missing: - self.parser.error(_("you need to use --missing if you use --no-rsvp")) - if not self.args.missing: - prefilled = {} - else: - # we get prefilled data with all people - try: - affiliations = await self.host.bridge.ps_node_affiliations_get( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get node affiliations: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - # we fill all affiliations with empty data, answered one will be filled - # below. We only consider people with "publisher" affiliation as invited, - # creators are not, and members can just observe - prefilled = { - jid_: {} - for jid_, affiliation in affiliations.items() - if affiliation in ("publisher",) - } - - try: - event_data = await self.host.bridge.event_invitees_list( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get event data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - # we fill nicknames and keep only requested people - - if self.args.no_rsvp: - for jid_ in event_data: - # if there is a jid in event_data it must be there in prefilled too - # otherwie somebody is not on the invitees list - try: - del prefilled[jid_] - except KeyError: - self.disp( - A.color( - C.A_WARNING, - f"We got a RSVP from somebody who was not in invitees " - f"list: {jid_}", - ), - error=True, - ) - else: - # we replace empty dicts for existing people with R.S.V.P. data - prefilled.update(event_data) - - # we get nicknames for everybody, make it easier for organisers - for jid_, data in prefilled.items(): - id_data = await self.host.bridge.identity_get(jid_, [], True, self.profile) - id_data = data_format.deserialise(id_data) - data["nick"] = id_data["nicknames"][0] - - await self.output(prefilled) - self.host.quit() - - -class InviteeInvite(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "invite", - use_pubsub=True, - pubsub_flags={C.NODE, C.SINGLE_ITEM}, - help=_("invite someone to the event through email"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-e", - "--email", - action="append", - default=[], - help="email(s) to send the invitation to", - ) - self.parser.add_argument( - "-N", - "--name", - default="", - help="name of the invitee", - ) - self.parser.add_argument( - "-H", - "--host-name", - default="", - help="name of the host", - ) - self.parser.add_argument( - "-l", - "--lang", - default="", - help="main language spoken by the invitee", - ) - self.parser.add_argument( - "-U", - "--url-template", - default="", - help="template to construct the URL", - ) - self.parser.add_argument( - "-S", - "--subject", - default="", - help="subject of the invitation email (default: generic subject)", - ) - self.parser.add_argument( - "-b", - "--body", - default="", - help="body of the invitation email (default: generic body)", - ) - - async def start(self): - email = self.args.email[0] if self.args.email else None - emails_extra = self.args.email[1:] - - try: - await self.host.bridge.event_invite_by_email( - self.args.service, - self.args.node, - self.args.item, - email, - emails_extra, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url_template, - self.args.subject, - self.args.body, - self.args.profile, - ) - except Exception as e: - self.disp(f"can't create invitation: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class Invitee(base.CommandBase): - subcommands = (InviteeGet, InviteeSet, InviteesList, InviteeInvite) - - def __init__(self, host): - super(Invitee, self).__init__( - host, "invitee", use_profile=False, help=_("manage invities") - ) - - -class Event(base.CommandBase): - subcommands = (Get, Create, Modify, Invitee) - - def __init__(self, host): - super(Event, self).__init__( - host, "event", use_profile=False, help=_("event management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_file.py --- a/sat_frontends/jp/cmd_file.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1108 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from . import xmlui_manager -import sys -import os -import os.path -import tarfile -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat_frontends.tools import jid -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.tools.common import utils -from urllib.parse import urlparse -from pathlib import Path -import tempfile -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -import json - -__commands__ = ["File"] -DEFAULT_DEST = "downloaded_file" - - -class Send(base.CommandBase): - def __init__(self, host): - super(Send, self).__init__( - host, - "send", - use_progress=True, - use_verbose=True, - help=_("send a file to a contact"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "files", type=str, nargs="+", metavar="file", help=_("a list of file") - ) - self.parser.add_argument("jid", help=_("the destination jid")) - self.parser.add_argument( - "-b", "--bz2", action="store_true", help=_("make a bzip2 tarball") - ) - self.parser.add_argument( - "-d", - "--path", - help=("path to the directory where the file must be stored"), - ) - self.parser.add_argument( - "-N", - "--namespace", - help=("namespace of the file"), - ) - self.parser.add_argument( - "-n", - "--name", - default="", - help=("name to use (DEFAULT: use source file name)"), - ) - self.parser.add_argument( - "-e", - "--encrypt", - action="store_true", - help=_("end-to-end encrypt the file transfer") - ) - - async def on_progress_started(self, metadata): - self.disp(_("File copy started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("File sent successfully"), 2) - - async def on_progress_error(self, error_msg): - if error_msg == C.PROGRESS_ERROR_DECLINED: - self.disp(_("The file has been refused by your contact")) - else: - self.disp(_("Error while sending file: {}").format(error_msg), error=True) - - async def got_id(self, data, file_): - """Called when a progress id has been received - - @param pid(unicode): progress id - @param file_(str): file path - """ - # FIXME: this show progress only for last progress_id - self.disp(_("File request sent to {jid}".format(jid=self.args.jid)), 1) - try: - await self.set_progress_id(data["progress"]) - except KeyError: - # TODO: if 'xmlui' key is present, manage xmlui message display - self.disp(_("Can't send file to {jid}".format(jid=self.args.jid)), error=True) - self.host.quit(2) - - async def start(self): - for file_ in self.args.files: - if not os.path.exists(file_): - self.disp( - _("file {file_} doesn't exist!").format(file_=repr(file_)), error=True - ) - self.host.quit(C.EXIT_BAD_ARG) - if not self.args.bz2 and os.path.isdir(file_): - self.disp( - _( - "{file_} is a dir! Please send files inside or use compression" - ).format(file_=repr(file_)) - ) - self.host.quit(C.EXIT_BAD_ARG) - - extra = {} - if self.args.path: - extra["path"] = self.args.path - if self.args.namespace: - extra["namespace"] = self.args.namespace - if self.args.encrypt: - extra["encrypted"] = True - - if self.args.bz2: - with tempfile.NamedTemporaryFile("wb", delete=False) as buf: - self.host.add_on_quit_callback(os.unlink, buf.name) - self.disp(_("bz2 is an experimental option, use with caution")) - # FIXME: check free space - self.disp(_("Starting compression, please wait...")) - sys.stdout.flush() - bz2 = tarfile.open(mode="w:bz2", fileobj=buf) - archive_name = "{}.tar.bz2".format( - os.path.basename(self.args.files[0]) or "compressed_files" - ) - for file_ in self.args.files: - self.disp(_("Adding {}").format(file_), 1) - bz2.add(file_) - bz2.close() - self.disp(_("Done !"), 1) - - try: - send_data = await self.host.bridge.file_send( - self.args.jid, - buf.name, - self.args.name or archive_name, - "", - data_format.serialise(extra), - self.profile, - ) - except Exception as e: - self.disp(f"can't send file: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.got_id(send_data, file_) - else: - for file_ in self.args.files: - path = os.path.abspath(file_) - try: - send_data = await self.host.bridge.file_send( - self.args.jid, - path, - self.args.name, - "", - data_format.serialise(extra), - self.profile, - ) - except Exception as e: - self.disp(f"can't send file {file_!r}: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.got_id(send_data, file_) - - -class Request(base.CommandBase): - def __init__(self, host): - super(Request, self).__init__( - host, - "request", - use_progress=True, - use_verbose=True, - help=_("request a file from a contact"), - ) - - @property - def filename(self): - return self.args.name or self.args.hash or "output" - - def add_parser_options(self): - self.parser.add_argument("jid", help=_("the destination jid")) - self.parser.add_argument( - "-D", - "--dest", - help=_( - "destination path where the file will be saved (default: " - "[current_dir]/[name|hash])" - ), - ) - self.parser.add_argument( - "-n", - "--name", - default="", - help=_("name of the file"), - ) - self.parser.add_argument( - "-H", - "--hash", - default="", - help=_("hash of the file"), - ) - self.parser.add_argument( - "-a", - "--hash-algo", - default="sha-256", - help=_("hash algorithm use for --hash (default: sha-256)"), - ) - self.parser.add_argument( - "-d", - "--path", - help=("path to the directory containing the file"), - ) - self.parser.add_argument( - "-N", - "--namespace", - help=("namespace of the file"), - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("overwrite existing file without confirmation"), - ) - - async def on_progress_started(self, metadata): - self.disp(_("File copy started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("File received successfully"), 2) - - async def on_progress_error(self, error_msg): - if error_msg == C.PROGRESS_ERROR_DECLINED: - self.disp(_("The file request has been refused")) - else: - self.disp(_("Error while requesting file: {}").format(error_msg), error=True) - - async def start(self): - if not self.args.name and not self.args.hash: - self.parser.error(_("at least one of --name or --hash must be provided")) - if self.args.dest: - path = os.path.abspath(os.path.expanduser(self.args.dest)) - if os.path.isdir(path): - path = os.path.join(path, self.filename) - else: - path = os.path.abspath(self.filename) - - if os.path.exists(path) and not self.args.force: - message = _("File {path} already exists! Do you want to overwrite?").format( - path=path - ) - await self.host.confirm_or_quit(message, _("file request cancelled")) - - self.full_dest_jid = await self.host.get_full_jid(self.args.jid) - extra = {} - if self.args.path: - extra["path"] = self.args.path - if self.args.namespace: - extra["namespace"] = self.args.namespace - try: - progress_id = await self.host.bridge.file_jingle_request( - self.full_dest_jid, - path, - self.args.name, - self.args.hash, - self.args.hash_algo if self.args.hash else "", - extra, - self.profile, - ) - except Exception as e: - self.disp(msg=_("can't request file: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.set_progress_id(progress_id) - - -class Receive(base.CommandAnswering): - def __init__(self, host): - super(Receive, self).__init__( - host, - "receive", - use_progress=True, - use_verbose=True, - help=_("wait for a file to be sent by a contact"), - ) - self._overwrite_refused = False # True when one overwrite as already been refused - self.action_callbacks = { - C.META_TYPE_FILE: self.on_file_action, - C.META_TYPE_OVERWRITE: self.on_overwrite_action, - C.META_TYPE_NOT_IN_ROSTER_LEAK: self.on_not_in_roster_action, - } - - def add_parser_options(self): - self.parser.add_argument( - "jids", - nargs="*", - help=_("jids accepted (accept everything if none is specified)"), - ) - self.parser.add_argument( - "-m", - "--multiple", - action="store_true", - help=_("accept multiple files (you'll have to stop manually)"), - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_( - "force overwritting of existing files (/!\\ name is choosed by sender)" - ), - ) - self.parser.add_argument( - "--path", - default=".", - metavar="DIR", - help=_("destination path (default: working directory)"), - ) - - async def on_progress_started(self, metadata): - self.disp(_("File copy started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("File received successfully"), 2) - if metadata.get("hash_verified", False): - try: - self.disp( - _("hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1 - ) - except KeyError: - self.disp(_("hash is checked but hash value is missing", 1), error=True) - else: - self.disp(_("hash can't be verified"), 1) - - async def on_progress_error(self, e): - self.disp(_("Error while receiving file: {e}").format(e=e), error=True) - - def get_xmlui_id(self, action_data): - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the futur - # TODO: XMLUI module - try: - xml_ui = action_data["xmlui"] - except KeyError: - self.disp(_("Action has no XMLUI"), 1) - else: - ui = ET.fromstring(xml_ui.encode("utf-8")) - xmlui_id = ui.get("submit") - if not xmlui_id: - self.disp(_("Invalid XMLUI received"), error=True) - return xmlui_id - - async def on_file_action(self, action_data, action_id, security_limit, profile): - xmlui_id = self.get_xmlui_id(action_data) - if xmlui_id is None: - return self.host.quit_from_signal(1) - try: - from_jid = jid.JID(action_data["from_jid"]) - except KeyError: - self.disp(_("Ignoring action without from_jid data"), 1) - return - try: - progress_id = action_data["progress_id"] - except KeyError: - self.disp(_("ignoring action without progress id"), 1) - return - - if not self.bare_jids or from_jid.bare in self.bare_jids: - if self._overwrite_refused: - self.disp(_("File refused because overwrite is needed"), error=True) - await self.host.bridge.action_launch( - xmlui_id, data_format.serialise({"cancelled": C.BOOL_TRUE}), - profile_key=profile - ) - return self.host.quit_from_signal(2) - await self.set_progress_id(progress_id) - xmlui_data = {"path": self.path} - await self.host.bridge.action_launch( - xmlui_id, data_format.serialise(xmlui_data), profile_key=profile - ) - - async def on_overwrite_action(self, action_data, action_id, security_limit, profile): - xmlui_id = self.get_xmlui_id(action_data) - if xmlui_id is None: - return self.host.quit_from_signal(1) - try: - progress_id = action_data["progress_id"] - except KeyError: - self.disp(_("ignoring action without progress id"), 1) - return - self.disp(_("Overwriting needed"), 1) - - if progress_id == self.progress_id: - if self.args.force: - self.disp(_("Overwrite accepted"), 2) - else: - self.disp(_("Refused to overwrite"), 2) - self._overwrite_refused = True - - xmlui_data = {"answer": C.bool_const(self.args.force)} - await self.host.bridge.action_launch( - xmlui_id, data_format.serialise(xmlui_data), profile_key=profile - ) - - async def on_not_in_roster_action(self, action_data, action_id, security_limit, profile): - xmlui_id = self.get_xmlui_id(action_data) - if xmlui_id is None: - return self.host.quit_from_signal(1) - try: - from_jid = jid.JID(action_data["from_jid"]) - except ValueError: - self.disp( - _('invalid "from_jid" value received, ignoring: {value}').format( - value=from_jid - ), - error=True, - ) - return - except KeyError: - self.disp(_('ignoring action without "from_jid" value'), error=True) - return - self.disp(_("Confirmation needed for request from an entity not in roster"), 1) - - if from_jid.bare in self.bare_jids: - # if the sender is expected, we can confirm the session - confirmed = True - self.disp(_("Sender confirmed because she or he is explicitly expected"), 1) - else: - xmlui = xmlui_manager.create(self.host, action_data["xmlui"]) - confirmed = await self.host.confirm(xmlui.dlg.message) - - xmlui_data = {"answer": C.bool_const(confirmed)} - await self.host.bridge.action_launch( - xmlui_id, data_format.serialise(xmlui_data), profile_key=profile - ) - if not confirmed and not self.args.multiple: - self.disp(_("Session refused for {from_jid}").format(from_jid=from_jid)) - self.host.quit_from_signal(0) - - async def start(self): - self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] - self.path = os.path.abspath(self.args.path) - if not os.path.isdir(self.path): - self.disp(_("Given path is not a directory !", error=True)) - self.host.quit(C.EXIT_BAD_ARG) - if self.args.multiple: - self.host.quit_on_progress_end = False - self.disp(_("waiting for incoming file request"), 2) - await self.start_answering() - - -class Get(base.CommandBase): - def __init__(self, host): - super(Get, self).__init__( - host, - "get", - use_progress=True, - use_verbose=True, - help=_("download a file from URI"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-o", - "--dest-file", - type=str, - default="", - help=_("destination file (DEFAULT: filename from URL)"), - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("overwrite existing file without confirmation"), - ) - self.parser.add_argument( - "attachment", type=str, - help=_("URI of the file to retrieve or JSON of the whole attachment") - ) - - async def on_progress_started(self, metadata): - self.disp(_("File download started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("File downloaded successfully"), 2) - - async def on_progress_error(self, error_msg): - self.disp(_("Error while downloading file: {}").format(error_msg), error=True) - - async def got_id(self, data): - """Called when a progress id has been received""" - try: - await self.set_progress_id(data["progress"]) - except KeyError: - if "xmlui" in data: - ui = xmlui_manager.create(self.host, data["xmlui"]) - await ui.show() - else: - self.disp(_("Can't download file"), error=True) - self.host.quit(C.EXIT_ERROR) - - async def start(self): - try: - attachment = json.loads(self.args.attachment) - except json.JSONDecodeError: - attachment = {"uri": self.args.attachment} - dest_file = self.args.dest_file - if not dest_file: - try: - dest_file = attachment["name"].replace("/", "-").strip() - except KeyError: - try: - dest_file = Path(urlparse(attachment["uri"]).path).name.strip() - except KeyError: - pass - if not dest_file: - dest_file = "downloaded_file" - - dest_file = Path(dest_file).expanduser().resolve() - if dest_file.exists() and not self.args.force: - message = _("File {path} already exists! Do you want to overwrite?").format( - path=dest_file - ) - await self.host.confirm_or_quit(message, _("file download cancelled")) - - options = {} - - try: - download_data_s = await self.host.bridge.file_download( - data_format.serialise(attachment), - str(dest_file), - data_format.serialise(options), - self.profile, - ) - download_data = data_format.deserialise(download_data_s) - except Exception as e: - self.disp(f"error while trying to download a file: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.got_id(download_data) - - -class Upload(base.CommandBase): - def __init__(self, host): - super(Upload, self).__init__( - host, "upload", use_progress=True, use_verbose=True, help=_("upload a file") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-e", - "--encrypt", - action="store_true", - help=_("encrypt file using AES-GCM"), - ) - self.parser.add_argument("file", type=str, help=_("file to upload")) - self.parser.add_argument( - "jid", - nargs="?", - help=_("jid of upload component (nothing to autodetect)"), - ) - self.parser.add_argument( - "--ignore-tls-errors", - action="store_true", - help=_(r"ignore invalide TLS certificate (/!\ Dangerous /!\)"), - ) - - async def on_progress_started(self, metadata): - self.disp(_("File upload started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("File uploaded successfully"), 2) - try: - url = metadata["url"] - except KeyError: - self.disp("download URL not found in metadata") - else: - self.disp(_("URL to retrieve the file:"), 1) - # XXX: url is displayed alone on a line to make parsing easier - self.disp(url) - - async def on_progress_error(self, error_msg): - self.disp(_("Error while uploading file: {}").format(error_msg), error=True) - - async def got_id(self, data, file_): - """Called when a progress id has been received - - @param pid(unicode): progress id - @param file_(str): file path - """ - try: - await self.set_progress_id(data["progress"]) - except KeyError: - if "xmlui" in data: - ui = xmlui_manager.create(self.host, data["xmlui"]) - await ui.show() - else: - self.disp(_("Can't upload file"), error=True) - self.host.quit(C.EXIT_ERROR) - - async def start(self): - file_ = self.args.file - if not os.path.exists(file_): - self.disp( - _("file {file_} doesn't exist !").format(file_=repr(file_)), error=True - ) - self.host.quit(C.EXIT_BAD_ARG) - if os.path.isdir(file_): - self.disp(_("{file_} is a dir! Can't upload a dir").format(file_=repr(file_))) - self.host.quit(C.EXIT_BAD_ARG) - - if self.args.jid is None: - self.full_dest_jid = "" - else: - self.full_dest_jid = await self.host.get_full_jid(self.args.jid) - - options = {} - if self.args.ignore_tls_errors: - options["ignore_tls_errors"] = True - if self.args.encrypt: - options["encryption"] = C.ENC_AES_GCM - - path = os.path.abspath(file_) - try: - upload_data = await self.host.bridge.file_upload( - path, - "", - self.full_dest_jid, - data_format.serialise(options), - self.profile, - ) - except Exception as e: - self.disp(f"error while trying to upload a file: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.got_id(upload_data, file_) - - -class ShareAffiliationsSet(base.CommandBase): - def __init__(self, host): - super(ShareAffiliationsSet, self).__init__( - host, - "set", - use_output=C.OUTPUT_DICT, - help=_("set affiliations for a shared file/directory"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-N", - "--namespace", - default="", - help=_("namespace of the repository"), - ) - self.parser.add_argument( - "-P", - "--path", - default="", - help=_("path to the repository"), - ) - self.parser.add_argument( - "-a", - "--affiliation", - dest="affiliations", - metavar=("JID", "AFFILIATION"), - required=True, - action="append", - nargs=2, - help=_("entity/affiliation couple(s)"), - ) - self.parser.add_argument( - "jid", - help=_("jid of file sharing entity"), - ) - - async def start(self): - affiliations = dict(self.args.affiliations) - try: - affiliations = await self.host.bridge.fis_affiliations_set( - self.args.jid, - self.args.namespace, - self.args.path, - affiliations, - self.profile, - ) - except Exception as e: - self.disp(f"can't set affiliations: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class ShareAffiliationsGet(base.CommandBase): - def __init__(self, host): - super(ShareAffiliationsGet, self).__init__( - host, - "get", - use_output=C.OUTPUT_DICT, - help=_("retrieve affiliations of a shared file/directory"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-N", - "--namespace", - default="", - help=_("namespace of the repository"), - ) - self.parser.add_argument( - "-P", - "--path", - default="", - help=_("path to the repository"), - ) - self.parser.add_argument( - "jid", - help=_("jid of sharing entity"), - ) - - async def start(self): - try: - affiliations = await self.host.bridge.fis_affiliations_get( - self.args.jid, - self.args.namespace, - self.args.path, - self.profile, - ) - except Exception as e: - self.disp(f"can't get affiliations: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(affiliations) - self.host.quit() - - -class ShareAffiliations(base.CommandBase): - subcommands = (ShareAffiliationsGet, ShareAffiliationsSet) - - def __init__(self, host): - super(ShareAffiliations, self).__init__( - host, "affiliations", use_profile=False, help=_("affiliations management") - ) - - -class ShareConfigurationSet(base.CommandBase): - def __init__(self, host): - super(ShareConfigurationSet, self).__init__( - host, - "set", - use_output=C.OUTPUT_DICT, - help=_("set configuration for a shared file/directory"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-N", - "--namespace", - default="", - help=_("namespace of the repository"), - ) - self.parser.add_argument( - "-P", - "--path", - default="", - help=_("path to the repository"), - ) - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="fields", - required=True, - metavar=("KEY", "VALUE"), - help=_("configuration field to set (required)"), - ) - self.parser.add_argument( - "jid", - help=_("jid of file sharing entity"), - ) - - async def start(self): - configuration = dict(self.args.fields) - try: - configuration = await self.host.bridge.fis_configuration_set( - self.args.jid, - self.args.namespace, - self.args.path, - configuration, - self.profile, - ) - except Exception as e: - self.disp(f"can't set configuration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class ShareConfigurationGet(base.CommandBase): - def __init__(self, host): - super(ShareConfigurationGet, self).__init__( - host, - "get", - use_output=C.OUTPUT_DICT, - help=_("retrieve configuration of a shared file/directory"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-N", - "--namespace", - default="", - help=_("namespace of the repository"), - ) - self.parser.add_argument( - "-P", - "--path", - default="", - help=_("path to the repository"), - ) - self.parser.add_argument( - "jid", - help=_("jid of sharing entity"), - ) - - async def start(self): - try: - configuration = await self.host.bridge.fis_configuration_get( - self.args.jid, - self.args.namespace, - self.args.path, - self.profile, - ) - except Exception as e: - self.disp(f"can't get configuration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(configuration) - self.host.quit() - - -class ShareConfiguration(base.CommandBase): - subcommands = (ShareConfigurationGet, ShareConfigurationSet) - - def __init__(self, host): - super(ShareConfiguration, self).__init__( - host, - "configuration", - use_profile=False, - help=_("file sharing node configuration"), - ) - - -class ShareList(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - super(ShareList, self).__init__( - host, - "list", - use_output=C.OUTPUT_LIST_DICT, - extra_outputs=extra_outputs, - help=_("retrieve files shared by an entity"), - use_verbose=True, - ) - - def add_parser_options(self): - self.parser.add_argument( - "-d", - "--path", - default="", - help=_("path to the directory containing the files"), - ) - self.parser.add_argument( - "jid", - nargs="?", - default="", - help=_("jid of sharing entity (nothing to check our own jid)"), - ) - - def _name_filter(self, name, row): - if row.type == C.FILE_TYPE_DIRECTORY: - return A.color(C.A_DIRECTORY, name) - elif row.type == C.FILE_TYPE_FILE: - return A.color(C.A_FILE, name) - else: - self.disp(_("unknown file type: {type}").format(type=row.type), error=True) - return name - - def _size_filter(self, size, row): - if not size: - return "" - return A.color(A.BOLD, utils.get_human_size(size)) - - def default_output(self, files_data): - """display files a way similar to ls""" - files_data.sort(key=lambda d: d["name"].lower()) - show_header = False - if self.verbosity == 0: - keys = headers = ("name", "type") - elif self.verbosity == 1: - keys = headers = ("name", "type", "size") - elif self.verbosity > 1: - show_header = True - keys = ("name", "type", "size", "file_hash") - headers = ("name", "type", "size", "hash") - table = common.Table.from_list_dict( - self.host, - files_data, - keys=keys, - headers=headers, - filters={"name": self._name_filter, "size": self._size_filter}, - defaults={"size": "", "file_hash": ""}, - ) - table.display_blank(show_header=show_header, hide_cols=["type"]) - - async def start(self): - try: - files_data = await self.host.bridge.fis_list( - self.args.jid, - self.args.path, - {}, - self.profile, - ) - except Exception as e: - self.disp(f"can't retrieve shared files: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - await self.output(files_data) - self.host.quit() - - -class SharePath(base.CommandBase): - def __init__(self, host): - super(SharePath, self).__init__( - host, "path", help=_("share a file or directory"), use_verbose=True - ) - - def add_parser_options(self): - self.parser.add_argument( - "-n", - "--name", - default="", - help=_("virtual name to use (default: use directory/file name)"), - ) - perm_group = self.parser.add_mutually_exclusive_group() - perm_group.add_argument( - "-j", - "--jid", - metavar="JID", - action="append", - dest="jids", - default=[], - help=_("jid of contacts allowed to retrieve the files"), - ) - perm_group.add_argument( - "--public", - action="store_true", - help=_( - r"share publicly the file(s) (/!\ *everybody* will be able to access " - r"them)" - ), - ) - self.parser.add_argument( - "path", - help=_("path to a file or directory to share"), - ) - - async def start(self): - self.path = os.path.abspath(self.args.path) - if self.args.public: - access = {"read": {"type": "public"}} - else: - jids = self.args.jids - if jids: - access = {"read": {"type": "whitelist", "jids": jids}} - else: - access = {} - try: - name = await self.host.bridge.fis_share_path( - self.args.name, - self.path, - json.dumps(access, ensure_ascii=False), - self.profile, - ) - except Exception as e: - self.disp(f"can't share path: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp( - _('{path} shared under the name "{name}"').format( - path=self.path, name=name - ) - ) - self.host.quit() - - -class ShareInvite(base.CommandBase): - def __init__(self, host): - super(ShareInvite, self).__init__( - host, "invite", help=_("send invitation for a shared repository") - ) - - def add_parser_options(self): - self.parser.add_argument( - "-n", - "--name", - default="", - help=_("name of the repository"), - ) - self.parser.add_argument( - "-N", - "--namespace", - default="", - help=_("namespace of the repository"), - ) - self.parser.add_argument( - "-P", - "--path", - help=_("path to the repository"), - ) - self.parser.add_argument( - "-t", - "--type", - choices=["files", "photos"], - default="files", - help=_("type of the repository"), - ) - self.parser.add_argument( - "-T", - "--thumbnail", - help=_("https URL of a image to use as thumbnail"), - ) - self.parser.add_argument( - "service", - help=_("jid of the file sharing service hosting the repository"), - ) - self.parser.add_argument( - "jid", - help=_("jid of the person to invite"), - ) - - async def start(self): - self.path = os.path.normpath(self.args.path) if self.args.path else "" - extra = {} - if self.args.thumbnail is not None: - if not self.args.thumbnail.startswith("http"): - self.parser.error(_("only http(s) links are allowed with --thumbnail")) - else: - extra["thumb_url"] = self.args.thumbnail - try: - await self.host.bridge.fis_invite( - self.args.jid, - self.args.service, - self.args.type, - self.args.namespace, - self.path, - self.args.name, - data_format.serialise(extra), - self.profile, - ) - except Exception as e: - self.disp(f"can't send invitation: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("invitation sent to {jid}").format(jid=self.args.jid)) - self.host.quit() - - -class Share(base.CommandBase): - subcommands = ( - ShareList, - SharePath, - ShareInvite, - ShareAffiliations, - ShareConfiguration, - ) - - def __init__(self, host): - super(Share, self).__init__( - host, "share", use_profile=False, help=_("files sharing management") - ) - - -class File(base.CommandBase): - subcommands = (Send, Request, Receive, Get, Upload, Share) - - def __init__(self, host): - super(File, self).__init__( - host, "file", use_profile=False, help=_("files sending/receiving/management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_forums.py --- a/sat_frontends/jp/cmd_forums.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from libervia.backend.tools.common.ansi import ANSI as A -import codecs -import json - -__commands__ = ["Forums"] - -FORUMS_TMP_DIR = "forums" - - -class Edit(base.CommandBase, common.BaseEdit): - use_items = False - - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "edit", - use_pubsub=True, - use_draft=True, - use_verbose=True, - help=_("edit forums"), - ) - common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR) - - def add_parser_options(self): - self.parser.add_argument( - "-k", - "--key", - default="", - help=_("forum key (DEFAULT: default forums)"), - ) - - def get_tmp_suff(self): - """return suffix used for content file""" - return "json" - - async def publish(self, forums_raw): - try: - await self.host.bridge.forums_set( - forums_raw, - self.args.service, - self.args.node, - self.args.key, - self.profile, - ) - except Exception as e: - self.disp(f"can't set forums: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("forums have been edited"), 1) - self.host.quit() - - async def start(self): - try: - forums_json = await self.host.bridge.forums_get( - self.args.service, - self.args.node, - self.args.key, - self.profile, - ) - except Exception as e: - if e.classname == "NotFound": - forums_json = "" - else: - self.disp(f"can't get node configuration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - content_file_obj, content_file_path = self.get_tmp_file() - forums_json = forums_json.strip() - if forums_json: - # we loads and dumps to have pretty printed json - forums = json.loads(forums_json) - # cf. https://stackoverflow.com/a/18337754 - f = codecs.getwriter("utf-8")(content_file_obj) - json.dump(forums, f, ensure_ascii=False, indent=4) - content_file_obj.seek(0) - await self.run_editor("forums_editor_args", content_file_path, content_file_obj) - - -class Get(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_COMPLEX, - extra_outputs=extra_outputs, - use_pubsub=True, - use_verbose=True, - help=_("get forums structure"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-k", - "--key", - default="", - help=_("forum key (DEFAULT: default forums)"), - ) - - def default_output(self, forums, level=0): - for forum in forums: - keys = list(forum.keys()) - keys.sort() - try: - keys.remove("title") - except ValueError: - pass - else: - keys.insert(0, "title") - try: - keys.remove("sub-forums") - except ValueError: - pass - else: - keys.append("sub-forums") - - for key in keys: - value = forum[key] - if key == "sub-forums": - self.default_output(value, level + 1) - else: - if self.host.verbosity < 1 and key != "title": - continue - head_color = C.A_LEVEL_COLORS[level % len(C.A_LEVEL_COLORS)] - self.disp( - A.color(level * 4 * " ", head_color, key, A.RESET, ": ", value) - ) - - async def start(self): - try: - forums_raw = await self.host.bridge.forums_get( - self.args.service, - self.args.node, - self.args.key, - self.profile, - ) - except Exception as e: - self.disp(f"can't get forums: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - if not forums_raw: - self.disp(_("no schema found"), 1) - self.host.quit(1) - forums = json.loads(forums_raw) - await self.output(forums) - self.host.quit() - - -class Forums(base.CommandBase): - subcommands = (Get, Edit) - - def __init__(self, host): - super(Forums, self).__init__( - host, "forums", use_profile=False, help=_("Forums structure edition") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_identity.py --- a/sat_frontends/jp/cmd_identity.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from libervia.backend.tools.common import data_format - -__commands__ = ["Identity"] - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_DICT, - use_verbose=True, - help=_("get identity data"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "--no-cache", action="store_true", help=_("do no use cached values") - ) - self.parser.add_argument( - "jid", help=_("entity to check") - ) - - async def start(self): - jid_ = (await self.host.check_jids([self.args.jid]))[0] - try: - data = await self.host.bridge.identity_get( - jid_, - [], - not self.args.no_cache, - self.profile - ) - except Exception as e: - self.disp(f"can't get identity data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - data = data_format.deserialise(data) - await self.output(data) - self.host.quit() - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__(host, "set", help=_("update identity data")) - - def add_parser_options(self): - self.parser.add_argument( - "-n", - "--nickname", - action="append", - metavar="NICKNAME", - dest="nicknames", - help=_("nicknames of the entity"), - ) - self.parser.add_argument( - "-d", - "--description", - help=_("description of the entity"), - ) - - async def start(self): - id_data = {} - for field in ("nicknames", "description"): - value = getattr(self.args, field) - if value is not None: - id_data[field] = value - if not id_data: - self.parser.error("At least one metadata must be set") - try: - self.host.bridge.identity_set( - data_format.serialise(id_data), - self.profile, - ) - except Exception as e: - self.disp(f"can't set identity data: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class Identity(base.CommandBase): - subcommands = (Get, Set) - - def __init__(self, host): - super(Identity, self).__init__( - host, "identity", use_profile=False, help=_("identity management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_info.py --- a/sat_frontends/jp/cmd_info.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,386 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from pprint import pformat - -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format, date_utils -from libervia.backend.tools.common.ansi import ANSI as A -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C - -from . import base - -__commands__ = ["Info"] - - -class Disco(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - super(Disco, self).__init__( - host, - "disco", - use_output="complex", - extra_outputs=extra_outputs, - help=_("service discovery"), - ) - - def add_parser_options(self): - self.parser.add_argument("jid", help=_("entity to discover")) - self.parser.add_argument( - "-t", - "--type", - type=str, - choices=("infos", "items", "both", "external", "all"), - default="all", - help=_("type of data to discover"), - ) - self.parser.add_argument("-n", "--node", default="", help=_("node to use")) - self.parser.add_argument( - "-C", - "--no-cache", - dest="use_cache", - action="store_false", - help=_("ignore cache"), - ) - - def default_output(self, data): - features = data.get("features", []) - identities = data.get("identities", []) - extensions = data.get("extensions", {}) - items = data.get("items", []) - external = data.get("external", []) - - identities_table = common.Table( - self.host, - identities, - headers=(_("category"), _("type"), _("name")), - use_buffer=True, - ) - - extensions_tpl = [] - extensions_types = list(extensions.keys()) - extensions_types.sort() - for type_ in extensions_types: - fields = [] - for field in extensions[type_]: - field_lines = [] - data, values = field - data_keys = list(data.keys()) - data_keys.sort() - for key in data_keys: - field_lines.append( - A.color("\t", C.A_SUBHEADER, key, A.RESET, ": ", data[key]) - ) - if len(values) == 1: - field_lines.append( - A.color( - "\t", - C.A_SUBHEADER, - "value", - A.RESET, - ": ", - values[0] or (A.BOLD + "UNSET"), - ) - ) - elif len(values) > 1: - field_lines.append( - A.color("\t", C.A_SUBHEADER, "values", A.RESET, ": ") - ) - - for value in values: - field_lines.append(A.color("\t - ", A.BOLD, value)) - fields.append("\n".join(field_lines)) - extensions_tpl.append( - "{type_}\n{fields}".format(type_=type_, fields="\n\n".join(fields)) - ) - - items_table = common.Table( - self.host, items, headers=(_("entity"), _("node"), _("name")), use_buffer=True - ) - - template = [] - fmt_kwargs = {} - if features: - template.append(A.color(C.A_HEADER, _("Features")) + "\n\n{features}") - if identities: - template.append(A.color(C.A_HEADER, _("Identities")) + "\n\n{identities}") - if extensions: - template.append(A.color(C.A_HEADER, _("Extensions")) + "\n\n{extensions}") - if items: - template.append(A.color(C.A_HEADER, _("Items")) + "\n\n{items}") - if external: - fmt_lines = [] - for e in external: - data = {k: e[k] for k in sorted(e)} - host = data.pop("host") - type_ = data.pop("type") - fmt_lines.append(A.color( - "\t", - C.A_SUBHEADER, - host, - " ", - A.RESET, - "[", - C.A_LEVEL_COLORS[1], - type_, - A.RESET, - "]", - )) - extended = data.pop("extended", None) - for key, value in data.items(): - fmt_lines.append(A.color( - "\t\t", - C.A_LEVEL_COLORS[2], - f"{key}: ", - C.A_LEVEL_COLORS[3], - str(value) - )) - if extended: - fmt_lines.append(A.color( - "\t\t", - C.A_HEADER, - "extended", - )) - nb_extended = len(extended) - for idx, form_data in enumerate(extended): - namespace = form_data.get("namespace") - if namespace: - fmt_lines.append(A.color( - "\t\t", - C.A_LEVEL_COLORS[2], - "namespace: ", - C.A_LEVEL_COLORS[3], - A.BOLD, - namespace - )) - for field_data in form_data["fields"]: - name = field_data.get("name") - if not name: - continue - field_type = field_data.get("type") - if "multi" in field_type: - value = ", ".join(field_data.get("values") or []) - else: - value = field_data.get("value") - if value is None: - continue - if field_type == "boolean": - value = C.bool(value) - fmt_lines.append(A.color( - "\t\t", - C.A_LEVEL_COLORS[2], - f"{name}: ", - C.A_LEVEL_COLORS[3], - A.BOLD, - str(value) - )) - if nb_extended>1 and idx < nb_extended-1: - fmt_lines.append("\n") - - fmt_lines.append("\n") - - template.append( - A.color(C.A_HEADER, _("External")) + "\n\n{external_formatted}" - ) - fmt_kwargs["external_formatted"] = "\n".join(fmt_lines) - - print( - "\n\n".join(template).format( - features="\n".join(features), - identities=identities_table.display().string, - extensions="\n".join(extensions_tpl), - items=items_table.display().string, - **fmt_kwargs, - ) - ) - - async def start(self): - infos_requested = self.args.type in ("infos", "both", "all") - items_requested = self.args.type in ("items", "both", "all") - exter_requested = self.args.type in ("external", "all") - if self.args.node: - if self.args.type == "external": - self.parser.error( - '--node can\'t be used with discovery of external services ' - '(--type="external")' - ) - else: - exter_requested = False - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - data = {} - - # infos - if infos_requested: - try: - infos = await self.host.bridge.disco_infos( - jid, - node=self.args.node, - use_cache=self.args.use_cache, - profile_key=self.host.profile, - ) - except Exception as e: - self.disp(_("error while doing discovery: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - else: - features, identities, extensions = infos - features.sort() - identities.sort(key=lambda identity: identity[2]) - data.update( - {"features": features, "identities": identities, "extensions": extensions} - ) - - # items - if items_requested: - try: - items = await self.host.bridge.disco_items( - jid, - node=self.args.node, - use_cache=self.args.use_cache, - profile_key=self.host.profile, - ) - except Exception as e: - self.disp(_("error while doing discovery: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - items.sort(key=lambda item: item[2]) - data["items"] = items - - # external - if exter_requested: - try: - ext_services_s = await self.host.bridge.external_disco_get( - jid, - self.host.profile, - ) - except Exception as e: - self.disp( - _("error while doing external service discovery: {e}").format(e=e), - error=True - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - data["external"] = data_format.deserialise( - ext_services_s, type_check=list - ) - - # output - await self.output(data) - self.host.quit() - - -class Version(base.CommandBase): - def __init__(self, host): - super(Version, self).__init__(host, "version", help=_("software version")) - - def add_parser_options(self): - self.parser.add_argument("jid", type=str, help=_("Entity to request")) - - async def start(self): - jids = await self.host.check_jids([self.args.jid]) - jid = jids[0] - try: - data = await self.host.bridge.software_version_get(jid, self.host.profile) - except Exception as e: - self.disp(_("error while trying to get version: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - infos = [] - name, version, os = data - if name: - infos.append(_("Software name: {name}").format(name=name)) - if version: - infos.append(_("Software version: {version}").format(version=version)) - if os: - infos.append(_("Operating System: {os}").format(os=os)) - - print("\n".join(infos)) - self.host.quit() - - -class Session(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - super(Session, self).__init__( - host, - "session", - use_output="dict", - extra_outputs=extra_outputs, - help=_("running session"), - ) - - def add_parser_options(self): - pass - - async def default_output(self, data): - started = data["started"] - data["started"] = "{short} (UTC, {relative})".format( - short=date_utils.date_fmt(started), - relative=date_utils.date_fmt(started, "relative"), - ) - await self.host.output(C.OUTPUT_DICT, "simple", {}, data) - - async def start(self): - try: - data = await self.host.bridge.session_infos_get(self.host.profile) - except Exception as e: - self.disp(_("Error getting session infos: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(data) - self.host.quit() - - -class Devices(base.CommandBase): - def __init__(self, host): - super(Devices, self).__init__( - host, "devices", use_output=C.OUTPUT_LIST_DICT, help=_("devices of an entity") - ) - - def add_parser_options(self): - self.parser.add_argument( - "jid", type=str, nargs="?", default="", help=_("Entity to request") - ) - - async def start(self): - try: - data = await self.host.bridge.devices_infos_get( - self.args.jid, self.host.profile - ) - except Exception as e: - self.disp(_("Error getting devices infos: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - data = data_format.deserialise(data, type_check=list) - await self.output(data) - self.host.quit() - - -class Info(base.CommandBase): - subcommands = (Disco, Version, Session, Devices) - - def __init__(self, host): - super(Info, self).__init__( - host, - "info", - use_profile=False, - help=_("Get various pieces of information on entities"), - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_input.py --- a/sat_frontends/jp/cmd_input.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,350 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import subprocess -import argparse -import sys -import shlex -import asyncio -from . import base -from libervia.backend.core.i18n import _ -from libervia.backend.core import exceptions -from sat_frontends.jp.constants import Const as C -from libervia.backend.tools.common.ansi import ANSI as A - -__commands__ = ["Input"] -OPT_STDIN = "stdin" -OPT_SHORT = "short" -OPT_LONG = "long" -OPT_POS = "positional" -OPT_IGNORE = "ignore" -OPT_TYPES = (OPT_STDIN, OPT_SHORT, OPT_LONG, OPT_POS, OPT_IGNORE) -OPT_EMPTY_SKIP = "skip" -OPT_EMPTY_IGNORE = "ignore" -OPT_EMPTY_CHOICES = (OPT_EMPTY_SKIP, OPT_EMPTY_IGNORE) - - -class InputCommon(base.CommandBase): - def __init__(self, host, name, help): - base.CommandBase.__init__( - self, host, name, use_verbose=True, use_profile=False, help=help - ) - self.idx = 0 - self.reset() - - def reset(self): - self.args_idx = 0 - self._stdin = [] - self._opts = [] - self._pos = [] - self._values_ori = [] - - def add_parser_options(self): - self.parser.add_argument( - "--encoding", default="utf-8", help=_("encoding of the input data") - ) - self.parser.add_argument( - "-i", - "--stdin", - action="append_const", - const=(OPT_STDIN, None), - dest="arguments", - help=_("standard input"), - ) - self.parser.add_argument( - "-s", - "--short", - type=self.opt(OPT_SHORT), - action="append", - dest="arguments", - help=_("short option"), - ) - self.parser.add_argument( - "-l", - "--long", - type=self.opt(OPT_LONG), - action="append", - dest="arguments", - help=_("long option"), - ) - self.parser.add_argument( - "-p", - "--positional", - type=self.opt(OPT_POS), - action="append", - dest="arguments", - help=_("positional argument"), - ) - self.parser.add_argument( - "-x", - "--ignore", - action="append_const", - const=(OPT_IGNORE, None), - dest="arguments", - help=_("ignore value"), - ) - self.parser.add_argument( - "-D", - "--debug", - action="store_true", - help=_("don't actually run commands but echo what would be launched"), - ) - self.parser.add_argument( - "--log", type=argparse.FileType("w"), help=_("log stdout to FILE") - ) - self.parser.add_argument( - "--log-err", type=argparse.FileType("w"), help=_("log stderr to FILE") - ) - self.parser.add_argument("command", nargs=argparse.REMAINDER) - - def opt(self, type_): - return lambda s: (type_, s) - - def add_value(self, value): - """add a parsed value according to arguments sequence""" - self._values_ori.append(value) - arguments = self.args.arguments - try: - arg_type, arg_name = arguments[self.args_idx] - except IndexError: - self.disp( - _("arguments in input data and in arguments sequence don't match"), - error=True, - ) - self.host.quit(C.EXIT_DATA_ERROR) - self.args_idx += 1 - while self.args_idx < len(arguments): - next_arg = arguments[self.args_idx] - if next_arg[0] not in OPT_TYPES: - # value will not be used if False or None, so we skip filter - if value not in (False, None): - # we have a filter - filter_type, filter_arg = arguments[self.args_idx] - value = self.filter(filter_type, filter_arg, value) - else: - break - self.args_idx += 1 - - if value is None: - # we ignore this argument - return - - if value is False: - # we skip the whole row - if self.args.debug: - self.disp( - A.color( - C.A_SUBHEADER, - _("values: "), - A.RESET, - ", ".join(self._values_ori), - ), - 2, - ) - self.disp(A.color(A.BOLD, _("**SKIPPING**\n"))) - self.reset() - self.idx += 1 - raise exceptions.CancelError - - if not isinstance(value, list): - value = [value] - - for v in value: - if arg_type == OPT_STDIN: - self._stdin.append(v) - elif arg_type == OPT_SHORT: - self._opts.append("-{}".format(arg_name)) - self._opts.append(v) - elif arg_type == OPT_LONG: - self._opts.append("--{}".format(arg_name)) - self._opts.append(v) - elif arg_type == OPT_POS: - self._pos.append(v) - elif arg_type == OPT_IGNORE: - pass - else: - self.parser.error( - _( - "Invalid argument, an option type is expected, got {type_}:{name}" - ).format(type_=arg_type, name=arg_name) - ) - - async def runCommand(self): - """run requested command with parsed arguments""" - if self.args_idx != len(self.args.arguments): - self.disp( - _("arguments in input data and in arguments sequence don't match"), - error=True, - ) - self.host.quit(C.EXIT_DATA_ERROR) - end = '\n' if self.args.debug else ' ' - self.disp( - A.color(C.A_HEADER, _("command {idx}").format(idx=self.idx)), - end = end, - ) - stdin = "".join(self._stdin) - if self.args.debug: - self.disp( - A.color( - C.A_SUBHEADER, - _("values: "), - A.RESET, - ", ".join([shlex.quote(a) for a in self._values_ori]) - ), - 2, - ) - - if stdin: - self.disp(A.color(C.A_SUBHEADER, "--- STDIN ---")) - self.disp(stdin) - self.disp(A.color(C.A_SUBHEADER, "-------------")) - - self.disp( - "{indent}{prog} {static} {options} {positionals}".format( - indent=4 * " ", - prog=sys.argv[0], - static=" ".join(self.args.command), - options=" ".join(shlex.quote(o) for o in self._opts), - positionals=" ".join(shlex.quote(p) for p in self._pos), - ) - ) - self.disp("\n") - else: - self.disp(" (" + ", ".join(self._values_ori) + ")", 2, end=' ') - args = [sys.argv[0]] + self.args.command + self._opts + self._pos - p = await asyncio.create_subprocess_exec( - *args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = await p.communicate(stdin.encode('utf-8')) - log = self.args.log - log_err = self.args.log_err - log_tpl = "{command}\n{buff}\n\n" - if log: - log.write(log_tpl.format( - command=" ".join(shlex.quote(a) for a in args), - buff=stdout.decode('utf-8', 'replace'))) - if log_err: - log_err.write(log_tpl.format( - command=" ".join(shlex.quote(a) for a in args), - buff=stderr.decode('utf-8', 'replace'))) - ret = p.returncode - if ret == 0: - self.disp(A.color(C.A_SUCCESS, _("OK"))) - else: - self.disp(A.color(C.A_FAILURE, _("FAILED"))) - - self.reset() - self.idx += 1 - - def filter(self, filter_type, filter_arg, value): - """change input value - - @param filter_type(unicode): name of the filter - @param filter_arg(unicode, None): argument of the filter - @param value(unicode): value to filter - @return (unicode, False, None): modified value - False to skip the whole row - None to ignore this argument (but continue row with other ones) - """ - raise NotImplementedError - - -class Csv(InputCommon): - def __init__(self, host): - super(Csv, self).__init__(host, "csv", _("comma-separated values")) - - def add_parser_options(self): - InputCommon.add_parser_options(self) - self.parser.add_argument( - "-r", - "--row", - type=int, - default=0, - help=_("starting row (previous ones will be ignored)"), - ) - self.parser.add_argument( - "-S", - "--split", - action="append_const", - const=("split", None), - dest="arguments", - help=_("split value in several options"), - ) - self.parser.add_argument( - "-E", - "--empty", - action="append", - type=self.opt("empty"), - dest="arguments", - help=_("action to do on empty value ({choices})").format( - choices=", ".join(OPT_EMPTY_CHOICES) - ), - ) - - def filter(self, filter_type, filter_arg, value): - if filter_type == "split": - return value.split() - elif filter_type == "empty": - if filter_arg == OPT_EMPTY_IGNORE: - return value if value else None - elif filter_arg == OPT_EMPTY_SKIP: - return value if value else False - else: - self.parser.error( - _("--empty value must be one of {choices}").format( - choices=", ".join(OPT_EMPTY_CHOICES) - ) - ) - - super(Csv, self).filter(filter_type, filter_arg, value) - - async def start(self): - import csv - - if self.args.encoding: - sys.stdin.reconfigure(encoding=self.args.encoding, errors="replace") - reader = csv.reader(sys.stdin) - for idx, row in enumerate(reader): - try: - if idx < self.args.row: - continue - for value in row: - self.add_value(value) - await self.runCommand() - except exceptions.CancelError: - #  this row has been cancelled, we skip it - continue - - self.host.quit() - - -class Input(base.CommandBase): - subcommands = (Csv,) - - def __init__(self, host): - super(Input, self).__init__( - host, - "input", - use_profile=False, - help=_("launch command with external input"), - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_invitation.py --- a/sat_frontends/jp/cmd_invitation.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,371 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.tools.common import data_format - -__commands__ = ["Invitation"] - - -class Create(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "create", - use_profile=False, - use_output=C.OUTPUT_DICT, - help=_("create and send an invitation"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-j", - "--jid", - default="", - help="jid of the invitee (default: generate one)", - ) - self.parser.add_argument( - "-P", - "--password", - default="", - help="password of the invitee profile/XMPP account (default: generate one)", - ) - self.parser.add_argument( - "-n", - "--name", - default="", - help="name of the invitee", - ) - self.parser.add_argument( - "-N", - "--host-name", - default="", - help="name of the host", - ) - self.parser.add_argument( - "-e", - "--email", - action="append", - default=[], - help="email(s) to send the invitation to (if --no-email is set, email will just be saved)", - ) - self.parser.add_argument( - "--no-email", action="store_true", help="do NOT send invitation email" - ) - self.parser.add_argument( - "-l", - "--lang", - default="", - help="main language spoken by the invitee", - ) - self.parser.add_argument( - "-u", - "--url", - default="", - help="template to construct the URL", - ) - self.parser.add_argument( - "-s", - "--subject", - default="", - help="subject of the invitation email (default: generic subject)", - ) - self.parser.add_argument( - "-b", - "--body", - default="", - help="body of the invitation email (default: generic body)", - ) - self.parser.add_argument( - "-x", - "--extra", - metavar=("KEY", "VALUE"), - action="append", - nargs=2, - default=[], - help="extra data to associate with invitation/invitee", - ) - self.parser.add_argument( - "-p", - "--profile", - default="", - help="profile doing the invitation (default: don't associate profile)", - ) - - async def start(self): - extra = dict(self.args.extra) - email = self.args.email[0] if self.args.email else None - emails_extra = self.args.email[1:] - if self.args.no_email: - if email: - extra["email"] = email - data_format.iter2dict("emails_extra", emails_extra) - else: - if not email: - self.parser.error( - _("you need to specify an email address to send email invitation") - ) - - try: - invitation_data = await self.host.bridge.invitation_create( - email, - emails_extra, - self.args.jid, - self.args.password, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url, - self.args.subject, - self.args.body, - extra, - self.args.profile, - ) - except Exception as e: - self.disp(f"can't create invitation: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(invitation_data) - self.host.quit(C.EXIT_OK) - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_profile=False, - use_output=C.OUTPUT_DICT, - help=_("get invitation data"), - ) - - def add_parser_options(self): - self.parser.add_argument("id", help=_("invitation UUID")) - self.parser.add_argument( - "-j", - "--with-jid", - action="store_true", - help=_("start profile session and retrieve jid"), - ) - - async def output_data(self, data, jid_=None): - if jid_ is not None: - data["jid"] = jid_ - await self.output(data) - self.host.quit() - - async def start(self): - try: - invitation_data = await self.host.bridge.invitation_get( - self.args.id, - ) - except Exception as e: - self.disp(msg=_("can't get invitation data: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - if not self.args.with_jid: - await self.output_data(invitation_data) - else: - profile = invitation_data["guest_profile"] - try: - await self.host.bridge.profile_start_session( - invitation_data["password"], - profile, - ) - except Exception as e: - self.disp(msg=_("can't start session: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - try: - jid_ = await self.host.bridge.param_get_a_async( - "JabberID", - "Connection", - profile_key=profile, - ) - except Exception as e: - self.disp(msg=_("can't retrieve jid: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - await self.output_data(invitation_data, jid_) - - -class Delete(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "delete", - use_profile=False, - help=_("delete guest account"), - ) - - def add_parser_options(self): - self.parser.add_argument("id", help=_("invitation UUID")) - - async def start(self): - try: - await self.host.bridge.invitation_delete( - self.args.id, - ) - except Exception as e: - self.disp(msg=_("can't delete guest account: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.host.quit() - - -class Modify(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, host, "modify", use_profile=False, help=_("modify existing invitation") - ) - - def add_parser_options(self): - self.parser.add_argument( - "--replace", action="store_true", help="replace the whole data" - ) - self.parser.add_argument( - "-n", - "--name", - default="", - help="name of the invitee", - ) - self.parser.add_argument( - "-N", - "--host-name", - default="", - help="name of the host", - ) - self.parser.add_argument( - "-e", - "--email", - default="", - help="email to send the invitation to (if --no-email is set, email will just be saved)", - ) - self.parser.add_argument( - "-l", - "--lang", - dest="language", - default="", - help="main language spoken by the invitee", - ) - self.parser.add_argument( - "-x", - "--extra", - metavar=("KEY", "VALUE"), - action="append", - nargs=2, - default=[], - help="extra data to associate with invitation/invitee", - ) - self.parser.add_argument( - "-p", - "--profile", - default="", - help="profile doing the invitation (default: don't associate profile", - ) - self.parser.add_argument("id", help=_("invitation UUID")) - - async def start(self): - extra = dict(self.args.extra) - for arg_name in ("name", "host_name", "email", "language", "profile"): - value = getattr(self.args, arg_name) - if not value: - continue - if arg_name in extra: - self.parser.error( - _( - "you can't set {arg_name} in both optional argument and extra" - ).format(arg_name=arg_name) - ) - extra[arg_name] = value - try: - await self.host.bridge.invitation_modify( - self.args.id, - extra, - self.args.replace, - ) - except Exception as e: - self.disp(f"can't modify invitation: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("invitations have been modified successfuly")) - self.host.quit(C.EXIT_OK) - - -class List(base.CommandBase): - def __init__(self, host): - extra_outputs = {"default": self.default_output} - base.CommandBase.__init__( - self, - host, - "list", - use_profile=False, - use_output=C.OUTPUT_COMPLEX, - extra_outputs=extra_outputs, - help=_("list invitations data"), - ) - - def default_output(self, data): - for idx, datum in enumerate(data.items()): - if idx: - self.disp("\n") - key, invitation_data = datum - self.disp(A.color(C.A_HEADER, key)) - indent = " " - for k, v in invitation_data.items(): - self.disp(indent + A.color(C.A_SUBHEADER, k + ":") + " " + str(v)) - - def add_parser_options(self): - self.parser.add_argument( - "-p", - "--profile", - default=C.PROF_KEY_NONE, - help=_("return only invitations linked to this profile"), - ) - - async def start(self): - try: - data = await self.host.bridge.invitation_list( - self.args.profile, - ) - except Exception as e: - self.disp(f"return only invitations linked to this profile: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(data) - self.host.quit() - - -class Invitation(base.CommandBase): - subcommands = (Create, Get, Delete, Modify, List) - - def __init__(self, host): - super(Invitation, self).__init__( - host, - "invitation", - use_profile=False, - help=_("invitation of user(s) without XMPP account"), - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_list.py --- a/sat_frontends/jp/cmd_list.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,351 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import json -import os -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp import common -from sat_frontends.jp.constants import Const as C -from . import base - -__commands__ = ["List"] - -FIELDS_MAP = "mapping" - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_verbose=True, - use_pubsub=True, - pubsub_flags={C.MULTI_ITEMS}, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - use_output=C.OUTPUT_LIST_XMLUI, - help=_("get lists"), - ) - - def add_parser_options(self): - pass - - async def start(self): - await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) - try: - lists_data = data_format.deserialise( - await self.host.bridge.list_get( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - self.get_pubsub_extra(), - self.profile, - ), - type_check=list, - ) - except Exception as e: - self.disp(f"can't get lists: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(lists_data[0]) - self.host.quit(C.EXIT_OK) - - -class Set(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - help=_("set a list item"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs="+", - dest="fields", - required=True, - metavar=("NAME", "VALUES"), - help=_("field(s) to set (required)"), - ) - self.parser.add_argument( - "-U", - "--update", - choices=("auto", "true", "false"), - default="auto", - help=_("update existing item instead of replacing it (DEFAULT: auto)"), - ) - self.parser.add_argument( - "item", - nargs="?", - default="", - help=_("id, URL of the item to update, or nothing for new item"), - ) - - async def start(self): - await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) - if self.args.update == "auto": - # we update if we have a item id specified - update = bool(self.args.item) - else: - update = C.bool(self.args.update) - - values = {} - - for field_data in self.args.fields: - values.setdefault(field_data[0], []).extend(field_data[1:]) - - extra = {"update": update} - - try: - item_id = await self.host.bridge.list_set( - self.args.service, - self.args.node, - values, - "", - self.args.item, - data_format.serialise(extra), - self.profile, - ) - except Exception as e: - self.disp(f"can't set list item: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(f"item {str(item_id or self.args.item)!r} set successfully") - self.host.quit(C.EXIT_OK) - - -class Delete(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "delete", - use_pubsub=True, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - help=_("delete a list item"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", "--force", action="store_true", help=_("delete without confirmation") - ) - self.parser.add_argument( - "-N", "--notify", action="store_true", help=_("notify deletion") - ) - self.parser.add_argument( - "item", - help=_("id of the item to delete"), - ) - - async def start(self): - await common.fill_well_known_uri(self, os.getcwd(), "tickets", meta_map={}) - if not self.args.item: - self.parser.error(_("You need to specify a list item to delete")) - if not self.args.force: - message = _("Are you sure to delete list item {item_id} ?").format( - item_id=self.args.item - ) - await self.host.confirm_or_quit(message, _("item deletion cancelled")) - try: - await self.host.bridge.list_delete_item( - self.args.service, - self.args.node, - self.args.item, - self.args.notify, - self.profile, - ) - except Exception as e: - self.disp(_("can't delete item: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("item {item} has been deleted").format(item=self.args.item)) - self.host.quit(C.EXIT_OK) - - -class Import(base.CommandBase): - # TODO: factorize with blog/import - - def __init__(self, host): - super().__init__( - host, - "import", - use_progress=True, - use_verbose=True, - help=_("import tickets from external software/dataset"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "importer", - nargs="?", - help=_("importer name, nothing to display importers list"), - ) - self.parser.add_argument( - "-o", - "--option", - action="append", - nargs=2, - default=[], - metavar=("NAME", "VALUE"), - help=_("importer specific options (see importer description)"), - ) - self.parser.add_argument( - "-m", - "--map", - action="append", - nargs=2, - default=[], - metavar=("IMPORTED_FIELD", "DEST_FIELD"), - help=_( - "specified field in import data will be put in dest field (default: use " - "same field name, or ignore if it doesn't exist)" - ), - ) - self.parser.add_argument( - "-s", - "--service", - default="", - metavar="PUBSUB_SERVICE", - help=_("PubSub service where the items must be uploaded (default: server)"), - ) - self.parser.add_argument( - "-n", - "--node", - default="", - metavar="PUBSUB_NODE", - help=_( - "PubSub node where the items must be uploaded (default: tickets' " - "defaults)" - ), - ) - self.parser.add_argument( - "location", - nargs="?", - help=_( - "importer data location (see importer description), nothing to show " - "importer description" - ), - ) - - async def on_progress_started(self, metadata): - self.disp(_("Tickets upload started"), 2) - - async def on_progress_finished(self, metadata): - self.disp(_("Tickets uploaded successfully"), 2) - - async def on_progress_error(self, error_msg): - self.disp( - _("Error while uploading tickets: {error_msg}").format(error_msg=error_msg), - error=True, - ) - - async def start(self): - if self.args.location is None: - # no location, the list of importer or description is requested - for name in ("option", "service", "node"): - if getattr(self.args, name): - self.parser.error( - _( - "{name} argument can't be used without location argument" - ).format(name=name) - ) - if self.args.importer is None: - self.disp( - "\n".join( - [ - f"{name}: {desc}" - for name, desc in await self.host.bridge.ticketsImportList() - ] - ) - ) - else: - try: - short_desc, long_desc = await self.host.bridge.ticketsImportDesc( - self.args.importer - ) - except Exception as e: - self.disp(f"can't get importer description: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(f"{name}: {short_desc}\n\n{long_desc}") - self.host.quit() - else: - # we have a location, an import is requested - - if self.args.progress: - # we use a custom progress bar template as we want a counter - self.pbar_template = [ - _("Progress: "), - ["Percentage"], - " ", - ["Bar"], - " ", - ["Counter"], - " ", - ["ETA"], - ] - - options = {key: value for key, value in self.args.option} - fields_map = dict(self.args.map) - if fields_map: - if FIELDS_MAP in options: - self.parser.error( - _( - "fields_map must be specified either preencoded in --option or " - "using --map, but not both at the same time" - ) - ) - options[FIELDS_MAP] = json.dumps(fields_map) - - try: - progress_id = await self.host.bridge.ticketsImport( - self.args.importer, - self.args.location, - options, - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp( - _("Error while trying to import tickets: {e}").format(e=e), - error=True, - ) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.set_progress_id(progress_id) - - -class List(base.CommandBase): - subcommands = (Get, Set, Delete, Import) - - def __init__(self, host): - super(List, self).__init__( - host, "list", use_profile=False, help=_("pubsub lists handling") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_merge_request.py --- a/sat_frontends/jp/cmd_merge_request.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import os.path -from . import base -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import xmlui_manager -from sat_frontends.jp import common - -__commands__ = ["MergeRequest"] - - -class Set(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - help=_("publish or update a merge request"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-i", - "--item", - default="", - help=_("id or URL of the request to update, or nothing for a new one"), - ) - self.parser.add_argument( - "-r", - "--repository", - metavar="PATH", - default=".", - help=_("path of the repository (DEFAULT: current directory)"), - ) - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("publish merge request without confirmation"), - ) - self.parser.add_argument( - "-l", - "--label", - dest="labels", - action="append", - help=_("labels to categorize your request"), - ) - - async def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - await common.fill_well_known_uri(self, self.repository, "merge requests") - if not self.args.force: - message = _( - "You are going to publish your changes to service " - "[{service}], are you sure ?" - ).format(service=self.args.service) - await self.host.confirm_or_quit( - message, _("merge request publication cancelled") - ) - - extra = {"update": True} if self.args.item else {} - values = {} - if self.args.labels is not None: - values["labels"] = self.args.labels - try: - published_id = await self.host.bridge.merge_request_set( - self.args.service, - self.args.node, - self.repository, - "auto", - values, - "", - self.args.item, - data_format.serialise(extra), - self.profile, - ) - except Exception as e: - self.disp(f"can't create merge requests: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - if published_id: - self.disp( - _("Merge request published at {published_id}").format( - published_id=published_id - ) - ) - else: - self.disp(_("Merge request published")) - - self.host.quit(C.EXIT_OK) - - -class Get(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_verbose=True, - use_pubsub=True, - pubsub_flags={C.MULTI_ITEMS}, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - help=_("get a merge request"), - ) - - def add_parser_options(self): - pass - - async def start(self): - await common.fill_well_known_uri(self, os.getcwd(), "merge requests", meta_map={}) - extra = {} - try: - requests_data = data_format.deserialise( - await self.host.bridge.merge_requests_get( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - "", - data_format.serialise(extra), - self.profile, - ) - ) - except Exception as e: - self.disp(f"can't get merge request: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - if self.verbosity >= 1: - whitelist = None - else: - whitelist = {"id", "title", "body"} - for request_xmlui in requests_data["items"]: - xmlui = xmlui_manager.create(self.host, request_xmlui, whitelist=whitelist) - await xmlui.show(values_only=True) - self.disp("") - self.host.quit(C.EXIT_OK) - - -class Import(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "import", - use_pubsub=True, - pubsub_flags={C.SINGLE_ITEM, C.ITEM}, - pubsub_defaults={"service": _("auto"), "node": _("auto")}, - help=_("import a merge request"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-r", - "--repository", - metavar="PATH", - default=".", - help=_("path of the repository (DEFAULT: current directory)"), - ) - - async def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - await common.fill_well_known_uri( - self, self.repository, "merge requests", meta_map={} - ) - extra = {} - try: - await self.host.bridge.merge_requests_import( - self.repository, - self.args.item, - self.args.service, - self.args.node, - extra, - self.profile, - ) - except Exception as e: - self.disp(f"can't import merge request: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class MergeRequest(base.CommandBase): - subcommands = (Set, Get, Import) - - def __init__(self, host): - super(MergeRequest, self).__init__( - host, "merge-request", use_profile=False, help=_("merge-request management") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_message.py --- a/sat_frontends/jp/cmd_message.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,327 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from pathlib import Path -import sys - -from twisted.python import filepath - -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.tools.utils import clean_ustr -from sat_frontends.jp import base -from sat_frontends.jp.constants import Const as C -from sat_frontends.tools import jid - - -__commands__ = ["Message"] - - -class Send(base.CommandBase): - def __init__(self, host): - super(Send, self).__init__(host, "send", help=_("send a message to a contact")) - - def add_parser_options(self): - self.parser.add_argument( - "-l", "--lang", type=str, default="", help=_("language of the message") - ) - self.parser.add_argument( - "-s", - "--separate", - action="store_true", - help=_( - "separate xmpp messages: send one message per line instead of one " - "message alone." - ), - ) - self.parser.add_argument( - "-n", - "--new-line", - action="store_true", - help=_( - "add a new line at the beginning of the input" - ), - ) - self.parser.add_argument( - "-S", - "--subject", - help=_("subject of the message"), - ) - self.parser.add_argument( - "-L", "--subject-lang", type=str, default="", help=_("language of subject") - ) - self.parser.add_argument( - "-t", - "--type", - choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,), - default=C.MESS_TYPE_AUTO, - help=_("type of the message"), - ) - self.parser.add_argument("-e", "--encrypt", metavar="ALGORITHM", - help=_("encrypt message using given algorithm")) - self.parser.add_argument( - "--encrypt-noreplace", - action="store_true", - help=_("don't replace encryption algorithm if an other one is already used")) - self.parser.add_argument( - "-a", "--attach", dest="attachments", action="append", metavar="FILE_PATH", - help=_("add a file as an attachment") - ) - syntax = self.parser.add_mutually_exclusive_group() - syntax.add_argument("-x", "--xhtml", action="store_true", help=_("XHTML body")) - syntax.add_argument("-r", "--rich", action="store_true", help=_("rich body")) - self.parser.add_argument( - "jid", help=_("the destination jid") - ) - - async def send_stdin(self, dest_jid): - """Send incomming data on stdin to jabber contact - - @param dest_jid: destination jid - """ - header = "\n" if self.args.new_line else "" - # FIXME: stdin is not read asynchronously at the moment - stdin_lines = [ - stream for stream in sys.stdin.readlines() - ] - extra = {} - if self.args.subject is None: - subject = {} - else: - subject = {self.args.subject_lang: self.args.subject} - - if self.args.xhtml or self.args.rich: - key = "xhtml" if self.args.xhtml else "rich" - if self.args.lang: - key = f"{key}_{self.args.lang}" - extra[key] = clean_ustr("".join(stdin_lines)) - stdin_lines = [] - - to_send = [] - - error = False - - if self.args.separate: - # we send stdin in several messages - if header: - # first we sent the header - try: - await self.host.bridge.message_send( - dest_jid, - {self.args.lang: header}, - subject, - self.args.type, - profile_key=self.profile, - ) - except Exception as e: - self.disp(f"can't send header: {e}", error=True) - error = True - - to_send.extend({self.args.lang: clean_ustr(l.replace("\n", ""))} - for l in stdin_lines) - else: - # we sent all in a single message - if not (self.args.xhtml or self.args.rich): - msg = {self.args.lang: header + clean_ustr("".join(stdin_lines))} - else: - msg = {} - to_send.append(msg) - - if self.args.attachments: - attachments = extra[C.KEY_ATTACHMENTS] = [] - for attachment in self.args.attachments: - try: - file_path = str(Path(attachment).resolve(strict=True)) - except FileNotFoundError: - self.disp("file {attachment} doesn't exists, ignoring", error=True) - else: - attachments.append({"path": file_path}) - - for idx, msg in enumerate(to_send): - if idx > 0 and C.KEY_ATTACHMENTS in extra: - # if we send several messages, we only want to send attachments with the - # first one - del extra[C.KEY_ATTACHMENTS] - try: - await self.host.bridge.message_send( - dest_jid, - msg, - subject, - self.args.type, - data_format.serialise(extra), - profile_key=self.host.profile) - except Exception as e: - self.disp(f"can't send message {msg!r}: {e}", error=True) - error = True - - if error: - # at least one message sending failed - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - self.host.quit() - - async def start(self): - if self.args.xhtml and self.args.separate: - self.disp( - "argument -s/--separate is not compatible yet with argument -x/--xhtml", - error=True, - ) - self.host.quit(C.EXIT_BAD_ARG) - - jids = await self.host.check_jids([self.args.jid]) - jid_ = jids[0] - - if self.args.encrypt_noreplace and self.args.encrypt is None: - self.parser.error("You need to use --encrypt if you use --encrypt-noreplace") - - if self.args.encrypt is not None: - try: - namespace = await self.host.bridge.encryption_namespace_get( - self.args.encrypt) - except Exception as e: - self.disp(f"can't get encryption namespace: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - try: - await self.host.bridge.message_encryption_start( - jid_, namespace, not self.args.encrypt_noreplace, self.profile - ) - except Exception as e: - self.disp(f"can't start encryption session: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - await self.send_stdin(jid_) - - -class Retract(base.CommandBase): - - def __init__(self, host): - super().__init__(host, "retract", help=_("retract a message")) - - def add_parser_options(self): - self.parser.add_argument( - "message_id", - help=_("ID of the message (internal ID)") - ) - - async def start(self): - try: - await self.host.bridge.message_retract( - self.args.message_id, - self.profile - ) - except Exception as e: - self.disp(f"can't retract message: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp( - "message retraction has been requested, please note that this is a " - "request which can't be enforced (see documentation for details).") - self.host.quit(C.EXIT_OK) - - -class MAM(base.CommandBase): - - def __init__(self, host): - super(MAM, self).__init__( - host, "mam", use_output=C.OUTPUT_MESS, use_verbose=True, - help=_("query archives using MAM")) - - def add_parser_options(self): - self.parser.add_argument( - "-s", "--service", default="", - help=_("jid of the service (default: profile's server")) - self.parser.add_argument( - "-S", "--start", dest="mam_start", type=base.date_decoder, - help=_( - "start fetching archive from this date (default: from the beginning)")) - self.parser.add_argument( - "-E", "--end", dest="mam_end", type=base.date_decoder, - help=_("end fetching archive after this date (default: no limit)")) - self.parser.add_argument( - "-W", "--with", dest="mam_with", - help=_("retrieve only archives with this jid")) - self.parser.add_argument( - "-m", "--max", dest="rsm_max", type=int, default=20, - help=_("maximum number of items to retrieve, using RSM (default: 20))")) - rsm_page_group = self.parser.add_mutually_exclusive_group() - rsm_page_group.add_argument( - "-a", "--after", dest="rsm_after", - help=_("find page after this item"), metavar='ITEM_ID') - rsm_page_group.add_argument( - "-b", "--before", dest="rsm_before", - help=_("find page before this item"), metavar='ITEM_ID') - rsm_page_group.add_argument( - "--index", dest="rsm_index", type=int, - help=_("index of the page to retrieve")) - - async def start(self): - extra = {} - if self.args.mam_start is not None: - extra["mam_start"] = float(self.args.mam_start) - if self.args.mam_end is not None: - extra["mam_end"] = float(self.args.mam_end) - if self.args.mam_with is not None: - extra["mam_with"] = self.args.mam_with - for suff in ('max', 'after', 'before', 'index'): - key = 'rsm_' + suff - value = getattr(self.args,key) - if value is not None: - extra[key] = str(value) - try: - data, metadata_s, profile = await self.host.bridge.mam_get( - self.args.service, data_format.serialise(extra), self.profile) - except Exception as e: - self.disp(f"can't retrieve MAM archives: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - metadata = data_format.deserialise(metadata_s) - - try: - session_info = await self.host.bridge.session_infos_get(self.profile) - except Exception as e: - self.disp(f"can't get session infos: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - # we need to fill own_jid for message output - self.host.own_jid = jid.JID(session_info["jid"]) - - await self.output(data) - - # FIXME: metadata are not displayed correctly and don't play nice with output - # they should be added to output data somehow - if self.verbosity: - for value in ("rsm_first", "rsm_last", "rsm_index", "rsm_count", - "mam_complete", "mam_stable"): - if value in metadata: - label = value.split("_")[1] - self.disp(A.color( - C.A_HEADER, label, ': ' , A.RESET, metadata[value])) - - self.host.quit() - - -class Message(base.CommandBase): - subcommands = (Send, Retract, MAM) - - def __init__(self, host): - super(Message, self).__init__( - host, "message", use_profile=False, help=_("messages handling") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_param.py --- a/sat_frontends/jp/cmd_param.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -from . import base -from libervia.backend.core.i18n import _ -from .constants import Const as C - -__commands__ = ["Param"] - - -class Get(base.CommandBase): - def __init__(self, host): - super(Get, self).__init__( - host, "get", need_connect=False, help=_("get a parameter value") - ) - - def add_parser_options(self): - self.parser.add_argument( - "category", nargs="?", help=_("category of the parameter") - ) - self.parser.add_argument("name", nargs="?", help=_("name of the parameter")) - self.parser.add_argument( - "-a", - "--attribute", - type=str, - default="value", - help=_("name of the attribute to get"), - ) - self.parser.add_argument( - "--security-limit", type=int, default=-1, help=_("security limit") - ) - - async def start(self): - if self.args.category is None: - categories = await self.host.bridge.params_categories_get() - print("\n".join(categories)) - elif self.args.name is None: - try: - values_dict = await self.host.bridge.params_values_from_category_get_async( - self.args.category, self.args.security_limit, "", "", self.profile - ) - except Exception as e: - self.disp( - _("can't find requested parameters: {e}").format(e=e), error=True - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - for name, value in values_dict.items(): - print(f"{name}\t{value}") - else: - try: - value = await self.host.bridge.param_get_a_async( - self.args.name, - self.args.category, - self.args.attribute, - self.args.security_limit, - self.profile, - ) - except Exception as e: - self.disp( - _("can't find requested parameter: {e}").format(e=e), error=True - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - print(value) - self.host.quit() - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__( - host, "set", need_connect=False, help=_("set a parameter value") - ) - - def add_parser_options(self): - self.parser.add_argument("category", help=_("category of the parameter")) - self.parser.add_argument("name", help=_("name of the parameter")) - self.parser.add_argument("value", help=_("name of the parameter")) - self.parser.add_argument( - "--security-limit", type=int, default=-1, help=_("security limit") - ) - - async def start(self): - try: - await self.host.bridge.param_set( - self.args.name, - self.args.value, - self.args.category, - self.args.security_limit, - self.profile, - ) - except Exception as e: - self.disp(_("can't set requested parameter: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class SaveTemplate(base.CommandBase): - # FIXME: this should probably be removed, it's not used and not useful for end-user - - def __init__(self, host): - super(SaveTemplate, self).__init__( - host, - "save", - use_profile=False, - help=_("save parameters template to xml file"), - ) - - def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("output file")) - - async def start(self): - """Save parameters template to XML file""" - try: - await self.host.bridge.params_template_save(self.args.filename) - except Exception as e: - self.disp(_("can't save parameters to file: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp( - _("parameters saved to file {filename}").format( - filename=self.args.filename - ) - ) - self.host.quit() - - -class LoadTemplate(base.CommandBase): - # FIXME: this should probably be removed, it's not used and not useful for end-user - - def __init__(self, host): - super(LoadTemplate, self).__init__( - host, - "load", - use_profile=False, - help=_("load parameters template from xml file"), - ) - - def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("input file")) - - async def start(self): - """Load parameters template from xml file""" - try: - self.host.bridge.params_template_load(self.args.filename) - except Exception as e: - self.disp(_("can't load parameters from file: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp( - _("parameters loaded from file {filename}").format( - filename=self.args.filename - ) - ) - self.host.quit() - - -class Param(base.CommandBase): - subcommands = (Get, Set, SaveTemplate, LoadTemplate) - - def __init__(self, host): - super(Param, self).__init__( - host, "param", use_profile=False, help=_("Save/load parameters template") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_ping.py --- a/sat_frontends/jp/cmd_ping.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from . import base -from libervia.backend.core.i18n import _ -from sat_frontends.jp.constants import Const as C - -__commands__ = ["Ping"] - - -class Ping(base.CommandBase): - def __init__(self, host): - super(Ping, self).__init__(host, "ping", help=_("ping XMPP entity")) - - def add_parser_options(self): - self.parser.add_argument("jid", help=_("jid to ping")) - self.parser.add_argument( - "-d", "--delay-only", action="store_true", help=_("output delay only (in s)") - ) - - async def start(self): - try: - pong_time = await self.host.bridge.ping(self.args.jid, self.profile) - except Exception as e: - self.disp(msg=_("can't do the ping: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - msg = pong_time if self.args.delay_only else f"PONG ({pong_time} s)" - self.disp(msg) - self.host.quit() diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_pipe.py --- a/sat_frontends/jp/cmd_pipe.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import asyncio -import errno -from functools import partial -import socket -import sys - -from libervia.backend.core.i18n import _ -from libervia.backend.tools.common import data_format -from sat_frontends.jp import base -from sat_frontends.jp import xmlui_manager -from sat_frontends.jp.constants import Const as C -from sat_frontends.tools import jid - -__commands__ = ["Pipe"] - -START_PORT = 9999 - - -class PipeOut(base.CommandBase): - def __init__(self, host): - super(PipeOut, self).__init__(host, "out", help=_("send a pipe a stream")) - - def add_parser_options(self): - self.parser.add_argument( - "jid", help=_("the destination jid") - ) - - async def start(self): - """ Create named pipe, and send stdin to it """ - try: - port = await self.host.bridge.stream_out( - await self.host.get_full_jid(self.args.jid), - self.profile, - ) - except Exception as e: - self.disp(f"can't start stream: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - # FIXME: we use temporarily blocking code here, as it simplify - # asyncio port: "loop.connect_read_pipe(lambda: reader_protocol, - # sys.stdin.buffer)" doesn't work properly when a file is piped in - # (we get a "ValueError: Pipe transport is for pipes/sockets only.") - # while it's working well for simple text sending. - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(("127.0.0.1", int(port))) - - while True: - buf = sys.stdin.buffer.read(4096) - if not buf: - break - try: - s.sendall(buf) - except socket.error as e: - if e.errno == errno.EPIPE: - sys.stderr.write(f"e\n") - self.host.quit(1) - else: - raise e - self.host.quit() - - -async def handle_stream_in(reader, writer, host): - """Write all received data to stdout""" - while True: - data = await reader.read(4096) - if not data: - break - sys.stdout.buffer.write(data) - try: - sys.stdout.flush() - except IOError as e: - sys.stderr.write(f"{e}\n") - break - host.quit_from_signal() - - -class PipeIn(base.CommandAnswering): - def __init__(self, host): - super(PipeIn, self).__init__(host, "in", help=_("receive a pipe stream")) - self.action_callbacks = {"STREAM": self.on_stream_action} - - def add_parser_options(self): - self.parser.add_argument( - "jids", - nargs="*", - help=_('Jids accepted (none means "accept everything")'), - ) - - def get_xmlui_id(self, action_data): - try: - xml_ui = action_data["xmlui"] - except KeyError: - self.disp(_("Action has no XMLUI"), 1) - else: - ui = xmlui_manager.create(self.host, xml_ui) - if not ui.submit_id: - self.disp(_("Invalid XMLUI received"), error=True) - self.quit_from_signal(C.EXIT_INTERNAL_ERROR) - return ui.submit_id - - async def on_stream_action(self, action_data, action_id, security_limit, profile): - xmlui_id = self.get_xmlui_id(action_data) - if xmlui_id is None: - self.host.quit_from_signal(C.EXIT_ERROR) - try: - from_jid = jid.JID(action_data["from_jid"]) - except KeyError: - self.disp(_("Ignoring action without from_jid data"), error=True) - return - - if not self.bare_jids or from_jid.bare in self.bare_jids: - host, port = "localhost", START_PORT - while True: - try: - server = await asyncio.start_server( - partial(handle_stream_in, host=self.host), host, port) - except socket.error as e: - if e.errno == errno.EADDRINUSE: - port += 1 - else: - raise e - else: - break - xmlui_data = {"answer": C.BOOL_TRUE, "port": str(port)} - await self.host.bridge.action_launch( - xmlui_id, data_format.serialise(xmlui_data), profile_key=profile - ) - async with server: - await server.serve_forever() - self.host.quit_from_signal() - - async def start(self): - self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] - await self.start_answering() - - -class Pipe(base.CommandBase): - subcommands = (PipeOut, PipeIn) - - def __init__(self, host): - super(Pipe, self).__init__( - host, "pipe", use_profile=False, help=_("stream piping through XMPP") - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_profile.py --- a/sat_frontends/jp/cmd_profile.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,280 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SAT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This module permits to manage profiles. It can list, create, delete -and retrieve information about a profile.""" - -from sat_frontends.jp.constants import Const as C -from libervia.backend.core.log import getLogger -from libervia.backend.core.i18n import _ -from sat_frontends.jp import base - -log = getLogger(__name__) - - -__commands__ = ["Profile"] - -PROFILE_HELP = _('The name of the profile') - - -class ProfileConnect(base.CommandBase): - """Dummy command to use profile_session parent, i.e. to be able to connect without doing anything else""" - - def __init__(self, host): - # it's weird to have a command named "connect" with need_connect=False, but it can be handy to be able - # to launch just the session, so some paradoxes don't hurt - super(ProfileConnect, self).__init__(host, 'connect', need_connect=False, help=('connect a profile')) - - def add_parser_options(self): - pass - - async def start(self): - # connection is already managed by profile common commands - # so we just need to check arguments and quit - if not self.args.connect and not self.args.start_session: - self.parser.error(_("You need to use either --connect or --start-session")) - self.host.quit() - -class ProfileDisconnect(base.CommandBase): - - def __init__(self, host): - super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=('disconnect a profile')) - - def add_parser_options(self): - pass - - async def start(self): - try: - await self.host.bridge.disconnect(self.args.profile) - except Exception as e: - self.disp(f"can't disconnect profile: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.host.quit() - - -class ProfileCreate(base.CommandBase): - def __init__(self, host): - super(ProfileCreate, self).__init__( - host, 'create', use_profile=False, help=('create a new profile')) - - def add_parser_options(self): - self.parser.add_argument('profile', type=str, help=_('the name of the profile')) - self.parser.add_argument( - '-p', '--password', type=str, default='', - help=_('the password of the profile')) - self.parser.add_argument( - '-j', '--jid', type=str, help=_('the jid of the profile')) - self.parser.add_argument( - '-x', '--xmpp-password', type=str, - help=_( - 'the password of the XMPP account (use profile password if not specified)' - ), - metavar='PASSWORD') - self.parser.add_argument( - '-A', '--autoconnect', choices=[C.BOOL_TRUE, C.BOOL_FALSE], nargs='?', - const=C.BOOL_TRUE, - help=_('connect this profile automatically when backend starts') - ) - self.parser.add_argument( - '-C', '--component', default='', - help=_('set to component import name (entry point) if this is a component')) - - async def start(self): - """Create a new profile""" - if self.args.profile in await self.host.bridge.profiles_list_get(): - self.disp(f"Profile {self.args.profile} already exists.", error=True) - self.host.quit(C.EXIT_BRIDGE_ERROR) - try: - await self.host.bridge.profile_create( - self.args.profile, self.args.password, self.args.component) - except Exception as e: - self.disp(f"can't create profile: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - try: - await self.host.bridge.profile_start_session( - self.args.password, self.args.profile) - except Exception as e: - self.disp(f"can't start profile session: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - if self.args.jid: - await self.host.bridge.param_set( - "JabberID", self.args.jid, "Connection", profile_key=self.args.profile) - xmpp_pwd = self.args.password or self.args.xmpp_password - if xmpp_pwd: - await self.host.bridge.param_set( - "Password", xmpp_pwd, "Connection", profile_key=self.args.profile) - - if self.args.autoconnect is not None: - await self.host.bridge.param_set( - "autoconnect_backend", self.args.autoconnect, "Connection", - profile_key=self.args.profile) - - self.disp(f'profile {self.args.profile} created successfully', 1) - self.host.quit() - - -class ProfileDefault(base.CommandBase): - def __init__(self, host): - super(ProfileDefault, self).__init__( - host, 'default', use_profile=False, help=('print default profile')) - - def add_parser_options(self): - pass - - async def start(self): - print(await self.host.bridge.profile_name_get('@DEFAULT@')) - self.host.quit() - - -class ProfileDelete(base.CommandBase): - def __init__(self, host): - super(ProfileDelete, self).__init__(host, 'delete', use_profile=False, help=('delete a profile')) - - def add_parser_options(self): - self.parser.add_argument('profile', type=str, help=PROFILE_HELP) - self.parser.add_argument('-f', '--force', action='store_true', help=_('delete profile without confirmation')) - - async def start(self): - if self.args.profile not in await self.host.bridge.profiles_list_get(): - log.error(f"Profile {self.args.profile} doesn't exist.") - self.host.quit(C.EXIT_NOT_FOUND) - if not self.args.force: - message = f"Are you sure to delete profile [{self.args.profile}] ?" - cancel_message = "Profile deletion cancelled" - await self.host.confirm_or_quit(message, cancel_message) - - await self.host.bridge.profile_delete_async(self.args.profile) - self.host.quit() - - -class ProfileInfo(base.CommandBase): - - def __init__(self, host): - super(ProfileInfo, self).__init__( - host, 'info', need_connect=False, use_output=C.OUTPUT_DICT, - help=_('get information about a profile')) - self.to_show = [(_("jid"), "Connection", "JabberID"),] - - def add_parser_options(self): - self.parser.add_argument( - '--show-password', action='store_true', - help=_('show the XMPP password IN CLEAR TEXT')) - - async def start(self): - if self.args.show_password: - self.to_show.append((_("XMPP password"), "Connection", "Password")) - self.to_show.append((_("autoconnect (backend)"), "Connection", - "autoconnect_backend")) - data = {} - for label, category, name in self.to_show: - try: - value = await self.host.bridge.param_get_a_async( - name, category, profile_key=self.host.profile) - except Exception as e: - self.disp(f"can't get {name}/{category} param: {e}", error=True) - else: - data[label] = value - - await self.output(data) - self.host.quit() - - -class ProfileList(base.CommandBase): - def __init__(self, host): - super(ProfileList, self).__init__( - host, 'list', use_profile=False, use_output='list', help=('list profiles')) - - def add_parser_options(self): - group = self.parser.add_mutually_exclusive_group() - group.add_argument( - '-c', '--clients', action='store_true', help=_('get clients profiles only')) - group.add_argument( - '-C', '--components', action='store_true', - help=('get components profiles only')) - - async def start(self): - if self.args.clients: - clients, components = True, False - elif self.args.components: - clients, components = False, True - else: - clients, components = True, True - await self.output(await self.host.bridge.profiles_list_get(clients, components)) - self.host.quit() - - -class ProfileModify(base.CommandBase): - - def __init__(self, host): - super(ProfileModify, self).__init__( - host, 'modify', need_connect=False, help=_('modify an existing profile')) - - def add_parser_options(self): - profile_pwd_group = self.parser.add_mutually_exclusive_group() - profile_pwd_group.add_argument( - '-w', '--password', help=_('change the password of the profile')) - profile_pwd_group.add_argument( - '--disable-password', action='store_true', - help=_('disable profile password (dangerous!)')) - self.parser.add_argument('-j', '--jid', help=_('the jid of the profile')) - self.parser.add_argument( - '-x', '--xmpp-password', help=_('change the password of the XMPP account'), - metavar='PASSWORD') - self.parser.add_argument( - '-D', '--default', action='store_true', help=_('set as default profile')) - self.parser.add_argument( - '-A', '--autoconnect', choices=[C.BOOL_TRUE, C.BOOL_FALSE], nargs='?', - const=C.BOOL_TRUE, - help=_('connect this profile automatically when backend starts') - ) - - async def start(self): - if self.args.disable_password: - self.args.password = '' - if self.args.password is not None: - await self.host.bridge.param_set( - "Password", self.args.password, "General", profile_key=self.host.profile) - if self.args.jid is not None: - await self.host.bridge.param_set( - "JabberID", self.args.jid, "Connection", profile_key=self.host.profile) - if self.args.xmpp_password is not None: - await self.host.bridge.param_set( - "Password", self.args.xmpp_password, "Connection", - profile_key=self.host.profile) - if self.args.default: - await self.host.bridge.profile_set_default(self.host.profile) - if self.args.autoconnect is not None: - await self.host.bridge.param_set( - "autoconnect_backend", self.args.autoconnect, "Connection", - profile_key=self.host.profile) - - self.host.quit() - - -class Profile(base.CommandBase): - subcommands = ( - ProfileConnect, ProfileDisconnect, ProfileCreate, ProfileDefault, ProfileDelete, - ProfileInfo, ProfileList, ProfileModify) - - def __init__(self, host): - super(Profile, self).__init__( - host, 'profile', use_profile=False, help=_('profile commands')) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/jp/cmd_pubsub.py --- a/sat_frontends/jp/cmd_pubsub.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3030 +0,0 @@ -#!/usr/bin/env python3 - - -# jp: a SàT command line tool -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -import argparse -import os.path -import re -import sys -import subprocess -import asyncio -import json -from . import base -from libervia.backend.core.i18n import _ -from libervia.backend.core import exceptions -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat_frontends.jp import arg_tools -from sat_frontends.jp import xml_tools -from functools import partial -from libervia.backend.tools.common import data_format -from libervia.backend.tools.common import uri -from libervia.backend.tools.common.ansi import ANSI as A -from libervia.backend.tools.common import date_utils -from sat_frontends.tools import jid, strings -from sat_frontends.bridge.bridge_frontend import BridgeException - -__commands__ = ["Pubsub"] - -PUBSUB_TMP_DIR = "pubsub" -PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema" -ALLOWED_SUBSCRIPTIONS_OWNER = ("subscribed", "pending", "none") - -# TODO: need to split this class in several modules, plugin should handle subcommands - - -class NodeInfo(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "info", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("retrieve node configuration"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-k", - "--key", - action="append", - dest="keys", - help=_("data key to filter"), - ) - - def remove_prefix(self, key): - return key[7:] if key.startswith("pubsub#") else key - - def filter_key(self, key): - return any((key == k or key == "pubsub#" + k) for k in self.args.keys) - - async def start(self): - try: - config_dict = await self.host.bridge.ps_node_configuration_get( - self.args.service, - self.args.node, - self.profile, - ) - except BridgeException as e: - if e.condition == "item-not-found": - self.disp( - f"The node {self.args.node} doesn't exist on {self.args.service}", - error=True, - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - self.disp(f"can't get node configuration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - else: - key_filter = (lambda k: True) if not self.args.keys else self.filter_key - config_dict = { - self.remove_prefix(k): v for k, v in config_dict.items() if key_filter(k) - } - await self.output(config_dict) - self.host.quit() - - -class NodeCreate(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "create", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("create a node"), - ) - - @staticmethod - def add_node_config_options(parser): - parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="fields", - default=[], - metavar=("KEY", "VALUE"), - help=_("configuration field to set"), - ) - parser.add_argument( - "-F", - "--full-prefix", - action="store_true", - help=_('don\'t prepend "pubsub#" prefix to field names'), - ) - - def add_parser_options(self): - self.add_node_config_options(self.parser) - - @staticmethod - def get_config_options(args): - if not args.full_prefix: - return {"pubsub#" + k: v for k, v in args.fields} - else: - return dict(args.fields) - - async def start(self): - options = self.get_config_options(self.args) - try: - node_id = await self.host.bridge.ps_node_create( - self.args.service, - self.args.node, - options, - self.profile, - ) - except Exception as e: - self.disp(msg=_("can't create node: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - if self.host.verbosity: - announce = _("node created successfully: ") - else: - announce = "" - self.disp(announce + node_id) - self.host.quit() - - -class NodePurge(base.CommandBase): - def __init__(self, host): - super(NodePurge, self).__init__( - host, - "purge", - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("purge a node (i.e. remove all items from it)"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("purge node without confirmation"), - ) - - async def start(self): - if not self.args.force: - if not self.args.service: - message = _( - "Are you sure to purge PEP node [{node}]? This will " - "delete ALL items from it!" - ).format(node=self.args.node) - else: - message = _( - "Are you sure to delete node [{node}] on service " - "[{service}]? This will delete ALL items from it!" - ).format(node=self.args.node, service=self.args.service) - await self.host.confirm_or_quit(message, _("node purge cancelled")) - - try: - await self.host.bridge.ps_node_purge( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(msg=_("can't purge node: {e}").format(e=e), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("node [{node}] purged successfully").format(node=self.args.node)) - self.host.quit() - - -class NodeDelete(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "delete", - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("delete a node"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--force", - action="store_true", - help=_("delete node without confirmation"), - ) - - async def start(self): - if not self.args.force: - if not self.args.service: - message = _("Are you sure to delete PEP node [{node}] ?").format( - node=self.args.node - ) - else: - message = _( - "Are you sure to delete node [{node}] on " "service [{service}]?" - ).format(node=self.args.node, service=self.args.service) - await self.host.confirm_or_quit(message, _("node deletion cancelled")) - - try: - await self.host.bridge.ps_node_delete( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't delete node: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("node [{node}] deleted successfully").format(node=self.args.node)) - self.host.quit() - - -class NodeSet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("set node configuration"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", - "--field", - action="append", - nargs=2, - dest="fields", - required=True, - metavar=("KEY", "VALUE"), - help=_("configuration field to set (required)"), - ) - self.parser.add_argument( - "-F", - "--full-prefix", - action="store_true", - help=_('don\'t prepend "pubsub#" prefix to field names'), - ) - - def get_key_name(self, k): - if self.args.full_prefix or k.startswith("pubsub#"): - return k - else: - return "pubsub#" + k - - async def start(self): - try: - await self.host.bridge.ps_node_configuration_set( - self.args.service, - self.args.node, - {self.get_key_name(k): v for k, v in self.args.fields}, - self.profile, - ) - except Exception as e: - self.disp(f"can't set node configuration: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("node configuration successful"), 1) - self.host.quit() - - -class NodeImport(base.CommandBase): - def __init__(self, host): - super(NodeImport, self).__init__( - host, - "import", - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("import raw XML to a node"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "--admin", - action="store_true", - help=_("do a pubsub admin request, needed to change publisher"), - ) - self.parser.add_argument( - "import_file", - type=argparse.FileType(), - help=_( - "path to the XML file with data to import. The file must contain " - "whole XML of each item to import." - ), - ) - - async def start(self): - try: - element, etree = xml_tools.etree_parse( - self, self.args.import_file, reraise=True - ) - except Exception as e: - from lxml.etree import XMLSyntaxError - - if isinstance(e, XMLSyntaxError) and e.code == 5: - # we have extra content, this probaby means that item are not wrapped - # so we wrap them here and try again - self.args.import_file.seek(0) - xml_buf = "" + self.args.import_file.read() + "" - element, etree = xml_tools.etree_parse(self, xml_buf) - - # we reverse element as we expect to have most recently published element first - # TODO: make this more explicit and add an option - element[:] = reversed(element) - - if not all([i.tag == "{http://jabber.org/protocol/pubsub}item" for i in element]): - self.disp( - _("You are not using list of pubsub items, we can't import this file"), - error=True, - ) - self.host.quit(C.EXIT_DATA_ERROR) - return - - items = [etree.tostring(i, encoding="unicode") for i in element] - if self.args.admin: - method = self.host.bridge.ps_admin_items_send - else: - self.disp( - _( - "Items are imported without using admin mode, publisher can't " - "be changed" - ) - ) - method = self.host.bridge.ps_items_send - - try: - items_ids = await method( - self.args.service, - self.args.node, - items, - "", - self.profile, - ) - except Exception as e: - self.disp(f"can't send items: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - if items_ids: - self.disp( - _("items published with id(s) {items_ids}").format( - items_ids=", ".join(items_ids) - ) - ) - else: - self.disp(_("items published")) - self.host.quit() - - -class NodeAffiliationsGet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("retrieve node affiliations (for node owner)"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - affiliations = await self.host.bridge.ps_node_affiliations_get( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get node affiliations: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(affiliations) - self.host.quit() - - -class NodeAffiliationsSet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("set affiliations (for node owner)"), - ) - - def add_parser_options(self): - # XXX: we use optional argument syntax for a required one because list of list of 2 elements - # (used to construct dicts) don't work with positional arguments - self.parser.add_argument( - "-a", - "--affiliation", - dest="affiliations", - metavar=("JID", "AFFILIATION"), - required=True, - action="append", - nargs=2, - help=_("entity/affiliation couple(s)"), - ) - - async def start(self): - affiliations = dict(self.args.affiliations) - try: - await self.host.bridge.ps_node_affiliations_set( - self.args.service, - self.args.node, - affiliations, - self.profile, - ) - except Exception as e: - self.disp(f"can't set node affiliations: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("affiliations have been set"), 1) - self.host.quit() - - -class NodeAffiliations(base.CommandBase): - subcommands = (NodeAffiliationsGet, NodeAffiliationsSet) - - def __init__(self, host): - super(NodeAffiliations, self).__init__( - host, - "affiliations", - use_profile=False, - help=_("set or retrieve node affiliations"), - ) - - -class NodeSubscriptionsGet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_DICT, - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("retrieve node subscriptions (for node owner)"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "--public", - action="store_true", - help=_("get public subscriptions"), - ) - - async def start(self): - if self.args.public: - method = self.host.bridge.ps_public_node_subscriptions_get - else: - method = self.host.bridge.ps_node_subscriptions_get - try: - subscriptions = await method( - self.args.service, - self.args.node, - self.profile, - ) - except Exception as e: - self.disp(f"can't get node subscriptions: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - await self.output(subscriptions) - self.host.quit() - - -class StoreSubscriptionAction(argparse.Action): - """Action which handle subscription parameter for owner - - list is given by pairs: jid and subscription state - if subscription state is not specified, it default to "subscribed" - """ - - def __call__(self, parser, namespace, values, option_string): - dest_dict = getattr(namespace, self.dest) - while values: - jid_s = values.pop(0) - try: - subscription = values.pop(0) - except IndexError: - subscription = "subscribed" - if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER: - parser.error( - _("subscription must be one of {}").format( - ", ".join(ALLOWED_SUBSCRIPTIONS_OWNER) - ) - ) - dest_dict[jid_s] = subscription - - -class NodeSubscriptionsSet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("set/modify subscriptions (for node owner)"), - ) - - def add_parser_options(self): - # XXX: we use optional argument syntax for a required one because list of list of 2 elements - # (uses to construct dicts) don't work with positional arguments - self.parser.add_argument( - "-S", - "--subscription", - dest="subscriptions", - default={}, - nargs="+", - metavar=("JID [SUSBSCRIPTION]"), - required=True, - action=StoreSubscriptionAction, - help=_("entity/subscription couple(s)"), - ) - - async def start(self): - try: - self.host.bridge.ps_node_subscriptions_set( - self.args.service, - self.args.node, - self.args.subscriptions, - self.profile, - ) - except Exception as e: - self.disp(f"can't set node subscriptions: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("subscriptions have been set"), 1) - self.host.quit() - - -class NodeSubscriptions(base.CommandBase): - subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet) - - def __init__(self, host): - super(NodeSubscriptions, self).__init__( - host, - "subscriptions", - use_profile=False, - help=_("get or modify node subscriptions"), - ) - - -class NodeSchemaSet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "set", - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("set/replace a schema"), - ) - - def add_parser_options(self): - self.parser.add_argument("schema", help=_("schema to set (must be XML)")) - - async def start(self): - try: - await self.host.bridge.ps_schema_set( - self.args.service, - self.args.node, - self.args.schema, - self.profile, - ) - except Exception as e: - self.disp(f"can't set schema: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("schema has been set"), 1) - self.host.quit() - - -class NodeSchemaEdit(base.CommandBase, common.BaseEdit): - use_items = False - - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "edit", - use_pubsub=True, - pubsub_flags={C.NODE}, - use_draft=True, - use_verbose=True, - help=_("edit a schema"), - ) - common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR) - - def add_parser_options(self): - pass - - async def publish(self, schema): - try: - await self.host.bridge.ps_schema_set( - self.args.service, - self.args.node, - schema, - self.profile, - ) - except Exception as e: - self.disp(f"can't set schema: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - else: - self.disp(_("schema has been set"), 1) - self.host.quit() - - async def ps_schema_get_cb(self, schema): - try: - from lxml import etree - except ImportError: - self.disp( - "lxml module must be installed to use edit, please install it " - 'with "pip install lxml"', - error=True, - ) - self.host.quit(1) - content_file_obj, content_file_path = self.get_tmp_file() - schema = schema.strip() - if schema: - parser = etree.XMLParser(remove_blank_text=True) - schema_elt = etree.fromstring(schema, parser) - content_file_obj.write( - etree.tostring(schema_elt, encoding="utf-8", pretty_print=True) - ) - content_file_obj.seek(0) - await self.run_editor( - "pubsub_schema_editor_args", content_file_path, content_file_obj - ) - - async def start(self): - try: - schema = await self.host.bridge.ps_schema_get( - self.args.service, - self.args.node, - self.profile, - ) - except BridgeException as e: - if e.condition == "item-not-found" or e.classname == "NotFound": - schema = "" - else: - self.disp(f"can't edit schema: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - await self.ps_schema_get_cb(schema) - - -class NodeSchemaGet(base.CommandBase): - def __init__(self, host): - base.CommandBase.__init__( - self, - host, - "get", - use_output=C.OUTPUT_XML, - use_pubsub=True, - pubsub_flags={C.NODE}, - use_verbose=True, - help=_("get schema"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - schema = await self.host.bridge.ps_schema_get( - self.args.service, - self.args.node, - self.profile, - ) - except BridgeException as e: - if e.condition == "item-not-found" or e.classname == "NotFound": - schema = None - else: - self.disp(f"can't get schema: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - if schema: - await self.output(schema) - self.host.quit() - else: - self.disp(_("no schema found"), 1) - self.host.quit(C.EXIT_NOT_FOUND) - - -class NodeSchema(base.CommandBase): - subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet) - - def __init__(self, host): - super(NodeSchema, self).__init__( - host, "schema", use_profile=False, help=_("data schema manipulation") - ) - - -class Node(base.CommandBase): - subcommands = ( - NodeInfo, - NodeCreate, - NodePurge, - NodeDelete, - NodeSet, - NodeImport, - NodeAffiliations, - NodeSubscriptions, - NodeSchema, - ) - - def __init__(self, host): - super(Node, self).__init__( - host, "node", use_profile=False, help=_("node handling") - ) - - -class CacheGet(base.CommandBase): - def __init__(self, host): - super().__init__( - host, - "get", - use_output=C.OUTPUT_LIST_XML, - use_pubsub=True, - pubsub_flags={C.NODE, C.MULTI_ITEMS, C.CACHE}, - help=_("get pubsub item(s) from cache"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-S", - "--sub-id", - default="", - help=_("subscription id"), - ) - - async def start(self): - try: - ps_result = data_format.deserialise( - await self.host.bridge.ps_cache_get( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - self.args.sub_id, - self.get_pubsub_extra(), - self.profile, - ) - ) - except BridgeException as e: - if e.classname == "NotFound": - self.disp( - f"The node {self.args.node} from {self.args.service} is not in cache " - f"for {self.profile}", - error=True, - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - self.disp(f"can't get pubsub items from cache: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - else: - await self.output(ps_result["items"]) - self.host.quit(C.EXIT_OK) - - -class CacheSync(base.CommandBase): - - def __init__(self, host): - super().__init__( - host, - "sync", - use_pubsub=True, - pubsub_flags={C.NODE}, - help=_("(re)synchronise a pubsub node"), - ) - - def add_parser_options(self): - pass - - async def start(self): - try: - await self.host.bridge.ps_cache_sync( - self.args.service, - self.args.node, - self.profile, - ) - except BridgeException as e: - if e.condition == "item-not-found" or e.classname == "NotFound": - self.disp( - f"The node {self.args.node} doesn't exist on {self.args.service}", - error=True, - ) - self.host.quit(C.EXIT_NOT_FOUND) - else: - self.disp(f"can't synchronise pubsub node: {e}", error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - else: - self.host.quit(C.EXIT_OK) - - -class CachePurge(base.CommandBase): - - def __init__(self, host): - super().__init__( - host, - "purge", - use_profile=False, - help=_("purge (delete) items from cache"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-s", "--service", action="append", metavar="JID", dest="services", - help="purge items only for these services. If not specified, items from ALL " - "services will be purged. May be used several times." - ) - self.parser.add_argument( - "-n", "--node", action="append", dest="nodes", - help="purge items only for these nodes. If not specified, items from ALL " - "nodes will be purged. May be used several times." - ) - self.parser.add_argument( - "-p", "--profile", action="append", dest="profiles", - help="purge items only for these profiles. If not specified, items from ALL " - "profiles will be purged. May be used several times." - ) - self.parser.add_argument( - "-b", "--updated-before", type=base.date_decoder, metavar="TIME_PATTERN", - help="purge items which have been last updated before given time." - ) - self.parser.add_argument( - "-C", "--created-before", type=base.date_decoder, metavar="TIME_PATTERN", - help="purge items which have been last created before given time." - ) - self.parser.add_argument( - "-t", "--type", action="append", dest="types", - help="purge items flagged with TYPE. May be used several times." - ) - self.parser.add_argument( - "-S", "--subtype", action="append", dest="subtypes", - help="purge items flagged with SUBTYPE. May be used several times." - ) - self.parser.add_argument( - "-f", "--force", action="store_true", - help=_("purge items without confirmation") - ) - - async def start(self): - if not self.args.force: - await self.host.confirm_or_quit( - _( - "Are you sure to purge items from cache? You'll have to bypass cache " - "or resynchronise nodes to access deleted items again." - ), - _("Items purgins has been cancelled.") - ) - purge_data = {} - for key in ( - "services", "nodes", "profiles", "updated_before", "created_before", - "types", "subtypes" - ): - value = getattr(self.args, key) - if value is not None: - purge_data[key] = value - try: - await self.host.bridge.ps_cache_purge( - data_format.serialise( - purge_data - ) - ) - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - else: - self.host.quit(C.EXIT_OK) - - -class CacheReset(base.CommandBase): - - def __init__(self, host): - super().__init__( - host, - "reset", - use_profile=False, - help=_("remove everything from cache"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", "--force", action="store_true", - help=_("reset cache without confirmation") - ) - - async def start(self): - if not self.args.force: - await self.host.confirm_or_quit( - _( - "Are you sure to reset cache? All nodes and items will be removed " - "from it, then it will be progressively refilled as if it were new. " - "This may be resources intensive." - ), - _("Pubsub cache reset has been cancelled.") - ) - try: - await self.host.bridge.ps_cache_reset() - except Exception as e: - self.disp(f"Internal error: {e}", error=True) - self.host.quit(C.EXIT_INTERNAL_ERROR) - else: - self.host.quit(C.EXIT_OK) - - -class CacheSearch(base.CommandBase): - def __init__(self, host): - extra_outputs = { - "default": self.default_output, - "xml": self.xml_output, - "xml-raw": self.xml_raw_output, - } - super().__init__( - host, - "search", - use_profile=False, - use_output=C.OUTPUT_LIST_DICT, - extra_outputs=extra_outputs, - help=_("search for pubsub items in cache"), - ) - - def add_parser_options(self): - self.parser.add_argument( - "-f", "--fts", help=_("Full-Text Search query"), metavar="FTS_QUERY" - ) - self.parser.add_argument( - "-p", "--profile", action="append", dest="profiles", metavar="PROFILE", - help="search items only from these profiles. May be used several times." - ) - self.parser.add_argument( - "-s", "--service", action="append", dest="services", metavar="SERVICE", - help="items must be from specified service. May be used several times." - ) - self.parser.add_argument( - "-n", "--node", action="append", dest="nodes", metavar="NODE", - help="items must be in the specified node. May be used several times." - ) - self.parser.add_argument( - "-t", "--type", action="append", dest="types", metavar="TYPE", - help="items must be of specified type. May be used several times." - ) - self.parser.add_argument( - "-S", "--subtype", action="append", dest="subtypes", metavar="SUBTYPE", - help="items must be of specified subtype. May be used several times." - ) - self.parser.add_argument( - "-P", "--payload", action="store_true", help=_("include item XML payload") - ) - self.parser.add_argument( - "-o", "--order-by", action="append", nargs="+", - metavar=("ORDER", "[FIELD] [DIRECTION]"), - help=_("how items must be ordered. May be used several times.") - ) - self.parser.add_argument( - "-l", "--limit", type=int, help=_("maximum number of items to return") - ) - self.parser.add_argument( - "-i", "--index", type=int, help=_("return results starting from this index") - ) - self.parser.add_argument( - "-F", - "--field", - action="append", - nargs=3, - dest="fields", - default=[], - metavar=("PATH", "OPERATOR", "VALUE"), - help=_("parsed data field filter. May be used several times."), - ) - self.parser.add_argument( - "-k", - "--key", - action="append", - dest="keys", - metavar="KEY", - help=_( - "data key(s) to display. May be used several times. DEFAULT: show all " - "keys" - ), - ) - - async def start(self): - query = {} - for arg in ("fts", "profiles", "services", "nodes", "types", "subtypes"): - value = getattr(self.args, arg) - if value: - if arg in ("types", "subtypes"): - # empty string is used to find items without type and/or subtype - value = [v or None for v in value] - query[arg] = value - for arg in ("limit", "index"): - value = getattr(self.args, arg) - if value is not None: - query[arg] = value - if self.args.order_by is not None: - for order_data in self.args.order_by: - order, *args = order_data - if order == "field": - if not args: - self.parser.error(_("field data must be specified in --order-by")) - elif len(args) == 1: - path = args[0] - direction = "asc" - elif len(args) == 2: - path, direction = args - else: - self.parser.error(_( - "You can't specify more that 2 arguments for a field in " - "--order-by" - )) - try: - path = json.loads(path) - except json.JSONDecodeError: - pass - order_query = { - "path": path, - } - else: - order_query = { - "order": order - } - if not args: - direction = "asc" - elif len(args) == 1: - direction = args[0] - else: - self.parser.error(_( - "there are too many arguments in --order-by option" - )) - if direction.lower() not in ("asc", "desc"): - self.parser.error(_("invalid --order-by direction: {direction!r}")) - order_query["direction"] = direction - query.setdefault("order-by", []).append(order_query) - - if self.args.fields: - parsed = [] - for field in self.args.fields: - path, operator, value = field - try: - path = json.loads(path) - except json.JSONDecodeError: - # this is not a JSON encoded value, we keep it as a string - pass - - if not isinstance(path, list): - path = [path] - - # handling of TP(
desc'), - "list": ("\n
  • ", "item 1", "
  • item 2
\n"), - "horizontalrule": ("\n
\n", "", ""), - "image": ('desc'), - }, -} - -# Define here the commands that are supported by the WYSIWYG edition. -# Keys must be the same than the ones used in RICH_SYNTAXES["XHTML"]. -# Values will be used to call execCommand(cmd, False, arg), they can be: -# - a string used for cmd and arg is assumed empty -# - a tuple (cmd, prompt, arg) with cmd the name of the command, -# prompt the text to display for asking a user input and arg is the -# value to use directly without asking the user if prompt is empty. -COMMANDS = { - "bold": "bold", - "italic": "italic", - "underline": "underline", - "code": ("formatBlock", "", "pre"), - "strikethrough": "strikeThrough", - "heading": ("heading", "Please specify the heading level (h1, h2, h3...)", ""), - "link": ("createLink", "Please specify an URL", ""), - "list": "insertUnorderedList", - "horizontalrule": "insertHorizontalRule", - "image": ("insertImage", "Please specify an image path", ""), -} - -# These values should be equal to the ones in plugin_misc_text_syntaxes -# FIXME: should the plugin import them from here to avoid duplicity? Importing -# the plugin's values from here is not possible because Libervia would fail. -PARAM_KEY_COMPOSITION = "Composition" -PARAM_NAME_SYNTAX = "Syntax" diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/css_color.py --- a/sat_frontends/tools/css_color.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 - - -# CSS color parsing -# Copyright (C) 2009-2021 Jérome-Poisson - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.log import getLogger - -log = getLogger(__name__) - - -CSS_COLORS = { - "black": "000000", - "silver": "c0c0c0", - "gray": "808080", - "white": "ffffff", - "maroon": "800000", - "red": "ff0000", - "purple": "800080", - "fuchsia": "ff00ff", - "green": "008000", - "lime": "00ff00", - "olive": "808000", - "yellow": "ffff00", - "navy": "000080", - "blue": "0000ff", - "teal": "008080", - "aqua": "00ffff", - "orange": "ffa500", - "aliceblue": "f0f8ff", - "antiquewhite": "faebd7", - "aquamarine": "7fffd4", - "azure": "f0ffff", - "beige": "f5f5dc", - "bisque": "ffe4c4", - "blanchedalmond": "ffebcd", - "blueviolet": "8a2be2", - "brown": "a52a2a", - "burlywood": "deb887", - "cadetblue": "5f9ea0", - "chartreuse": "7fff00", - "chocolate": "d2691e", - "coral": "ff7f50", - "cornflowerblue": "6495ed", - "cornsilk": "fff8dc", - "crimson": "dc143c", - "darkblue": "00008b", - "darkcyan": "008b8b", - "darkgoldenrod": "b8860b", - "darkgray": "a9a9a9", - "darkgreen": "006400", - "darkgrey": "a9a9a9", - "darkkhaki": "bdb76b", - "darkmagenta": "8b008b", - "darkolivegreen": "556b2f", - "darkorange": "ff8c00", - "darkorchid": "9932cc", - "darkred": "8b0000", - "darksalmon": "e9967a", - "darkseagreen": "8fbc8f", - "darkslateblue": "483d8b", - "darkslategray": "2f4f4f", - "darkslategrey": "2f4f4f", - "darkturquoise": "00ced1", - "darkviolet": "9400d3", - "deeppink": "ff1493", - "deepskyblue": "00bfff", - "dimgray": "696969", - "dimgrey": "696969", - "dodgerblue": "1e90ff", - "firebrick": "b22222", - "floralwhite": "fffaf0", - "forestgreen": "228b22", - "gainsboro": "dcdcdc", - "ghostwhite": "f8f8ff", - "gold": "ffd700", - "goldenrod": "daa520", - "greenyellow": "adff2f", - "grey": "808080", - "honeydew": "f0fff0", - "hotpink": "ff69b4", - "indianred": "cd5c5c", - "indigo": "4b0082", - "ivory": "fffff0", - "khaki": "f0e68c", - "lavender": "e6e6fa", - "lavenderblush": "fff0f5", - "lawngreen": "7cfc00", - "lemonchiffon": "fffacd", - "lightblue": "add8e6", - "lightcoral": "f08080", - "lightcyan": "e0ffff", - "lightgoldenrodyellow": "fafad2", - "lightgray": "d3d3d3", - "lightgreen": "90ee90", - "lightgrey": "d3d3d3", - "lightpink": "ffb6c1", - "lightsalmon": "ffa07a", - "lightseagreen": "20b2aa", - "lightskyblue": "87cefa", - "lightslategray": "778899", - "lightslategrey": "778899", - "lightsteelblue": "b0c4de", - "lightyellow": "ffffe0", - "limegreen": "32cd32", - "linen": "faf0e6", - "mediumaquamarine": "66cdaa", - "mediumblue": "0000cd", - "mediumorchid": "ba55d3", - "mediumpurple": "9370db", - "mediumseagreen": "3cb371", - "mediumslateblue": "7b68ee", - "mediumspringgreen": "00fa9a", - "mediumturquoise": "48d1cc", - "mediumvioletred": "c71585", - "midnightblue": "191970", - "mintcream": "f5fffa", - "mistyrose": "ffe4e1", - "moccasin": "ffe4b5", - "navajowhite": "ffdead", - "oldlace": "fdf5e6", - "olivedrab": "6b8e23", - "orangered": "ff4500", - "orchid": "da70d6", - "palegoldenrod": "eee8aa", - "palegreen": "98fb98", - "paleturquoise": "afeeee", - "palevioletred": "db7093", - "papayawhip": "ffefd5", - "peachpuff": "ffdab9", - "peru": "cd853f", - "pink": "ffc0cb", - "plum": "dda0dd", - "powderblue": "b0e0e6", - "rosybrown": "bc8f8f", - "royalblue": "4169e1", - "saddlebrown": "8b4513", - "salmon": "fa8072", - "sandybrown": "f4a460", - "seagreen": "2e8b57", - "seashell": "fff5ee", - "sienna": "a0522d", - "skyblue": "87ceeb", - "slateblue": "6a5acd", - "slategray": "708090", - "slategrey": "708090", - "snow": "fffafa", - "springgreen": "00ff7f", - "steelblue": "4682b4", - "tan": "d2b48c", - "thistle": "d8bfd8", - "tomato": "ff6347", - "turquoise": "40e0d0", - "violet": "ee82ee", - "wheat": "f5deb3", - "whitesmoke": "f5f5f5", - "yellowgreen": "9acd32", - "rebeccapurple": "663399", -} -DEFAULT = "000000" - - -def parse(raw_value, as_string=True): - """parse CSS color value and return normalised value - - @param raw_value(unicode): CSS value - @param as_string(bool): if True return a string, - else return a tuple of int - @return (unicode, tuple): normalised value - if as_string is True, value is 3 or 4 hex words (e.g. u"ff00aabb") - else value is a 3 or 4 tuple of int (e.g.: (255, 0, 170, 187)). - If present, the 4th value is the alpha channel - If value can't be parsed, a warning message is logged, and DEFAULT is returned - """ - raw_value = raw_value.strip().lower() - if raw_value.startswith("#"): - # we have a hexadecimal value - str_value = raw_value[1:] - if len(raw_value) in (3, 4): - str_value = "".join([2 * v for v in str_value]) - elif raw_value.startswith("rgb"): - left_p = raw_value.find("(") - right_p = raw_value.find(")") - rgb_values = [v.strip() for v in raw_value[left_p + 1 : right_p].split(",")] - expected_len = 4 if raw_value.startswith("rgba") else 3 - if len(rgb_values) != expected_len: - log.warning("incorrect value: {}".format(raw_value)) - str_value = DEFAULT - else: - int_values = [] - for rgb_v in rgb_values: - p_idx = rgb_v.find("%") - if p_idx == -1: - # base 10 value - try: - int_v = int(rgb_v) - if int_v > 255: - raise ValueError("value exceed 255") - int_values.append(int_v) - except ValueError: - log.warning("invalid int: {}".format(rgb_v)) - int_values.append(0) - else: - # percentage - try: - int_v = int(int(rgb_v[:p_idx]) / 100.0 * 255) - if int_v > 255: - raise ValueError("value exceed 255") - int_values.append(int_v) - except ValueError: - log.warning("invalid percent value: {}".format(rgb_v)) - int_values.append(0) - str_value = "".join(["{:02x}".format(v) for v in int_values]) - elif raw_value.startswith("hsl"): - log.warning("hue-saturation-lightness not handled yet") # TODO - str_value = DEFAULT - else: - try: - str_value = CSS_COLORS[raw_value] - except KeyError: - log.warning("unrecognised format: {}".format(raw_value)) - str_value = DEFAULT - - if as_string: - return str_value - else: - return tuple( - [ - int(str_value[i] + str_value[i + 1], 16) - for i in range(0, len(str_value), 2) - ] - ) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/games.py --- a/sat_frontends/tools/games.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 - - -# SAT: a jabber client -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This library help manage general games (e.g. card games) and it is shared by the frontends""" - -SUITS_ORDER = [ - "pique", - "coeur", - "trefle", - "carreau", - "atout", -] # I have switched the usual order 'trefle' and 'carreau' because card are more easy to see if suit colour change (black, red, black, red) -VALUES_ORDER = [str(i) for i in range(1, 11)] + ["valet", "cavalier", "dame", "roi"] - - -class TarotCard(object): - """This class is used to represent a car logically""" - - def __init__(self, tuple_card): - """@param tuple_card: tuple (suit, value)""" - self.suit, self.value = tuple_card - self.bout = self.suit == "atout" and self.value in ["1", "21", "excuse"] - if self.bout or self.value == "roi": - self.points = 4.5 - elif self.value == "dame": - self.points = 3.5 - elif self.value == "cavalier": - self.points = 2.5 - elif self.value == "valet": - self.points = 1.5 - else: - self.points = 0.5 - - def get_tuple(self): - return (self.suit, self.value) - - @staticmethod - def from_tuples(tuple_list): - result = [] - for card_tuple in tuple_list: - result.append(TarotCard(card_tuple)) - return result - - def __cmp__(self, other): - if other is None: - return 1 - if self.suit != other.suit: - idx1 = SUITS_ORDER.index(self.suit) - idx2 = SUITS_ORDER.index(other.suit) - return idx1.__cmp__(idx2) - if self.suit == "atout": - if self.value == other.value == "excuse": - return 0 - if self.value == "excuse": - return -1 - if other.value == "excuse": - return 1 - return int(self.value).__cmp__(int(other.value)) - # at this point we have the same suit which is not 'atout' - idx1 = VALUES_ORDER.index(self.value) - idx2 = VALUES_ORDER.index(other.value) - return idx1.__cmp__(idx2) - - def __str__(self): - return "[%s,%s]" % (self.suit, self.value) - - -# These symbols are diplayed by Libervia next to the player's nicknames -SYMBOLS = {"Radiocol": ["♬"], "Tarot": ["♠", "♣", "♥", "♦"]} diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/host_listener.py --- a/sat_frontends/tools/host_listener.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 - - -# SAT: a jabber client -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""This module is only used launch callbacks when host is ready, used for early initialisation stuffs""" - - -listeners = [] - - -def addListener(cb): - """Add a listener which will be called when host is ready - - @param cb: callback which will be called when host is ready with host as only argument - """ - listeners.append(cb) - - -def call_listeners(host): - """Must be called by frontend when host is ready. - - The call will launch all the callbacks, then remove the listeners list. - @param host(QuickApp): the instancied QuickApp subclass - """ - global listeners - while True: - try: - cb = listeners.pop(0) - cb(host) - except IndexError: - break - del listeners diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/jid.py --- a/sat_frontends/tools/jid.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 - -# Libervia XMPP -# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from typing import Optional, Tuple - - -class JID(str): - """This class helps manage JID (@/)""" - - def __new__(cls, jid_str: str) -> "JID": - return str.__new__(cls, cls._normalize(jid_str)) - - def __init__(self, jid_str: str): - self._node, self._domain, self._resource = self._parse() - - @staticmethod - def _normalize(jid_str: str) -> str: - """Naive normalization before instantiating and parsing the JID""" - if not jid_str: - return jid_str - tokens = jid_str.split("/") - tokens[0] = tokens[0].lower() # force node and domain to lower-case - return "/".join(tokens) - - def _parse(self) -> Tuple[Optional[str], str, Optional[str]]: - """Find node, domain, and resource from JID""" - node_end = self.find("@") - if node_end < 0: - node_end = 0 - domain_end = self.find("/") - if domain_end == 0: - raise ValueError("a jid can't start with '/'") - if domain_end == -1: - domain_end = len(self) - node = self[:node_end] or None - domain = self[(node_end + 1) if node_end else 0 : domain_end] - resource = self[domain_end + 1 :] or None - return node, domain, resource - - @property - def node(self) -> Optional[str]: - return self._node - - @property - def local(self) -> Optional[str]: - return self._node - - @property - def domain(self) -> str: - return self._domain - - @property - def resource(self) -> Optional[str]: - return self._resource - - @property - def bare(self) -> "JID": - if not self.node: - return JID(self.domain) - return JID(f"{self.node}@{self.domain}") - - def change_resource(self, resource: str) -> "JID": - """Build a new JID with the same node and domain but a different resource. - - @param resource: The new resource for the JID. - @return: A new JID instance with the updated resource. - """ - return JID(f"{self.bare}/{resource}") - - def is_valid(self) -> bool: - """ - @return: True if the JID is XMPP compliant - """ - # Simple check for domain part - if not self.domain or self.domain.startswith(".") or self.domain.endswith("."): - return False - if ".." in self.domain: - return False - return True - - -def new_resource(entity: JID, resource: str) -> JID: - """Build a new JID from the given entity and resource. - - @param entity: original JID - @param resource: new resource - @return: a new JID instance - """ - return entity.change_resource(resource) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/misc.py --- a/sat_frontends/tools/misc.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 - - -# SAT helpers methods for plugins -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - - -class InputHistory(object): - def _update_input_history(self, text=None, step=None, callback=None, mode=""): - """Update the lists of previously sent messages. Several lists can be - handled as they are stored in a dictionary, the argument "mode" being - used as the entry key. There's also a temporary list to allow you play - with previous entries before sending a new message. Parameters values - can be combined: text is None and step is None to initialize a main - list and the temporary one, step is None to update a list and - reinitialize the temporary one, step is not None to update - the temporary list between two messages. - @param text: text to be saved. - @param step: step to move the temporary index. - @param callback: method to display temporary entries. - @param mode: the dictionary key for main lists. - """ - if not hasattr(self, "input_histories"): - self.input_histories = {} - history = self.input_histories.setdefault(mode, []) - if step is None and text is not None: - # update the main list - if text in history: - history.remove(text) - history.append(text) - length = len(history) - if step is None or length == 0: - # prepare the temporary list and index - self.input_history_tmp = history[:] - self.input_history_tmp.append("") - self.input_history_index = length - if step is None: - return - # update the temporary list - if text is not None: - # save the current entry - self.input_history_tmp[self.input_history_index] = text - # move to another entry if possible - index_tmp = self.input_history_index + step - if index_tmp >= 0 and index_tmp < len(self.input_history_tmp): - if callback is not None: - callback(self.input_history_tmp[index_tmp]) - self.input_history_index = index_tmp - - -class FlagsHandler(object): - """Small class to handle easily option flags - - the instance is initialized with an iterable - then attribute return True if flag is set, False else. - """ - - def __init__(self, flags): - self.flags = set(flags or []) - self._used_flags = set() - - def __getattr__(self, flag): - self._used_flags.add(flag) - return flag in self.flags - - def __getitem__(self, flag): - return getattr(self, flag) - - def __len__(self): - return len(self.flags) - - def __iter__(self): - return self.flags.__iter__() - - @property - def all_used(self): - """Return True if all flags have been used""" - return self._used_flags.issuperset(self.flags) - - @property - def unused(self): - """Return flags which has not been used yet""" - return self.flags.difference(self._used_flags) diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/strings.py --- a/sat_frontends/tools/strings.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 - - -# SAT helpers methods for plugins -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import re - -# Regexp from http://daringfireball.net/2010/07/improved_regex_for_matching_urls -RE_URL = re.compile( - r"""(?i)\b((?:[a-z]{3,}://|(www|ftp)\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/|mailto:|xmpp:|geo:)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?]))""" -) - - -# TODO: merge this class with an other module or at least rename it (strings is not a good name) - - -def get_url_params(url): - """This comes from pyjamas.Location.makeUrlDict with a small change - to also parse full URLs, and parameters with no value specified - (in that case the default value "" is used). - @param url: any URL with or without parameters - @return: a dictionary of the parameters, if any was given, or {} - """ - dict_ = {} - if "/" in url: - # keep the part after the last "/" - url = url[url.rindex("/") + 1 :] - if url.startswith("?"): - # remove the first "?" - url = url[1:] - pairs = url.split("&") - for pair in pairs: - if len(pair) < 3: - continue - kv = pair.split("=", 1) - dict_[kv[0]] = kv[1] if len(kv) > 1 else "" - return dict_ - - -def add_url_to_text(string, new_target=True): - """Check a text for what looks like an URL and make it clickable. - - @param string (unicode): text to process - @param new_target (bool): if True, make the link open in a new window - """ - # XXX: report any change to libervia.browser.strings.add_url_to_text - def repl(match): - url = match.group(0) - if not re.match(r"""[a-z]{3,}://|mailto:|xmpp:""", url): - url = "http://" + url - target = ' target="_blank"' if new_target else "" - return '%s' % (url, target, match.group(0)) - - return RE_URL.sub(repl, string) - - -def add_url_to_image(string): - """Check a XHTML text for what looks like an imageURL and make it clickable. - - @param string (unicode): text to process - """ - # XXX: report any change to libervia.browser.strings.add_url_to_image - def repl(match): - url = match.group(1) - return '%s' % (url, match.group(0)) - - pattern = r"""]* src="([^"]+)"[^>]*>""" - return re.sub(pattern, repl, string) - - -def fix_xhtml_links(xhtml): - """Add http:// if the scheme is missing and force opening in a new window. - - @param string (unicode): XHTML Content - """ - subs = [] - for match in re.finditer(r'', xhtml): - tag = match.group(0) - url = re.search(r'href="([^"]*)"', tag) - if url and not url.group(1).startswith("#"): # skip internal anchor - if not re.search(r'target="([^"]*)"', tag): # no target - subs.append((tag, '. - -"""This library help manage XML used in SàT frontends """ - -# we don't import minidom as a different class can be used in frontends -# (e.g. NativeDOM in Libervia) - - -def inline_root(doc): - """ make the root attribute inline - @param root_node: minidom's Document compatible class - @return: plain XML - """ - root_elt = doc.documentElement - if root_elt.hasAttribute("style"): - styles_raw = root_elt.getAttribute("style") - styles = styles_raw.split(";") - new_styles = [] - for style in styles: - try: - key, value = style.split(":") - except ValueError: - continue - if key.strip().lower() == "display": - value = "inline" - new_styles.append("%s: %s" % (key.strip(), value.strip())) - root_elt.setAttribute("style", "; ".join(new_styles)) - else: - root_elt.setAttribute("style", "display: inline") - return root_elt.toxml() diff -r 7c5654c54fed -r 26b7ed2817da sat_frontends/tools/xmlui.py --- a/sat_frontends/tools/xmlui.py Fri Jun 02 12:59:21 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1149 +0,0 @@ -#!/usr/bin/env python3 - - -# SàT frontend tools -# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from libervia.backend.core.i18n import _ -from libervia.backend.core.log import getLogger - -log = getLogger(__name__) -from sat_frontends.quick_frontend.constants import Const as C -from libervia.backend.core import exceptions - - -_class_map = {} -CLASS_PANEL = "panel" -CLASS_DIALOG = "dialog" -CURRENT_LABEL = "current_label" -HIDDEN = "hidden" - - -class InvalidXMLUI(Exception): - pass - - -class ClassNotRegistedError(Exception): - pass - - -# FIXME: this method is duplicated in frontends.tools.xmlui.get_text -def get_text(node): - """Get child text nodes - @param node: dom Node - @return: joined unicode text of all nodes - - """ - data = [] - for child in node.childNodes: - if child.nodeType == child.TEXT_NODE: - data.append(child.wholeText) - return "".join(data) - - -class Widget(object): - """base Widget""" - - pass - - -class EmptyWidget(Widget): - """Just a placeholder widget""" - - pass - - -class TextWidget(Widget): - """Non interactive text""" - - pass - - -class LabelWidget(Widget): - """Non interactive text""" - - pass - - -class JidWidget(Widget): - """Jabber ID""" - - pass - - -class DividerWidget(Widget): - """Separator""" - - pass - - -class StringWidget(Widget): - """Input widget wich require a string - - often called Edit in toolkits - """ - - pass - - -class JidInputWidget(Widget): - """Input widget wich require a string - - often called Edit in toolkits - """ - - pass - - -class PasswordWidget(Widget): - """Input widget with require a masked string""" - - pass - - -class TextBoxWidget(Widget): - """Input widget with require a long, possibly multilines string - - often called TextArea in toolkits - """ - - pass - - -class XHTMLBoxWidget(Widget): - """Input widget specialised in XHTML editing, - - a WYSIWYG or specialised editor is expected - """ - - pass - - -class BoolWidget(Widget): - """Input widget with require a boolean value - often called CheckBox in toolkits - """ - - pass - - -class IntWidget(Widget): - """Input widget with require an integer""" - - pass - - -class ButtonWidget(Widget): - """A clickable widget""" - - pass - - -class ListWidget(Widget): - """A widget able to show/choose one or several strings in a list""" - - pass - - -class JidsListWidget(Widget): - """A widget able to show/choose one or several strings in a list""" - - pass - - -class Container(Widget): - """Widget which can contain other ones with a specific layout""" - - @classmethod - def _xmlui_adapt(cls, instance): - """Make cls as instance.__class__ - - cls must inherit from original instance class - Usefull when you get a class from UI toolkit - """ - assert instance.__class__ in cls.__bases__ - instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) - - -class PairsContainer(Container): - """Widgets are disposed in rows of two (usually label/input)""" - - pass - - -class LabelContainer(Container): - """Widgets are associated with label or empty widget""" - - pass - - -class TabsContainer(Container): - """A container which several other containers in tabs - - Often called Notebook in toolkits - """ - - pass - - -class VerticalContainer(Container): - """Widgets are disposed vertically""" - - pass - - -class AdvancedListContainer(Container): - """Widgets are disposed in rows with advaned features""" - - pass - - -class Dialog(object): - """base dialog""" - - def __init__(self, _xmlui_parent): - self._xmlui_parent = _xmlui_parent - - def _xmlui_validated(self, data=None): - if data is None: - data = {} - self._xmlui_set_data(C.XMLUI_STATUS_VALIDATED, data) - self._xmlui_submit(data) - - def _xmlui_cancelled(self): - data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} - self._xmlui_set_data(C.XMLUI_STATUS_CANCELLED, data) - self._xmlui_submit(data) - - def _xmlui_submit(self, data): - if self._xmlui_parent.submit_id is None: - log.debug(_("Nothing to submit")) - else: - self._xmlui_parent.submit(data) - - def _xmlui_set_data(self, status, data): - pass - - -class MessageDialog(Dialog): - """Dialog with a OK/Cancel type configuration""" - - pass - - -class NoteDialog(Dialog): - """Short message which doesn't need user confirmation to disappear""" - - pass - - -class ConfirmDialog(Dialog): - """Dialog with a OK/Cancel type configuration""" - - def _xmlui_set_data(self, status, data): - if status == C.XMLUI_STATUS_VALIDATED: - data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE - elif status == C.XMLUI_STATUS_CANCELLED: - data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE - - -class FileDialog(Dialog): - """Dialog with a OK/Cancel type configuration""" - - pass - - -class XMLUIBase(object): - """Base class to construct SàT XML User Interface - - This class must not be instancied directly - """ - - def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, - profile=C.PROF_KEY_NONE): - """Initialise the XMLUI instance - - @param host: %(doc_host)s - @param parsed_dom: main parsed dom - @param title: force the title, or use XMLUI one if None - @param flags: list of string which can be: - - NO_CANCEL: the UI can't be cancelled - - FROM_BACKEND: the UI come from backend (i.e. it's not the direct result of - user operation) - @param callback(callable, None): if not None, will be used with action_launch: - - if None is used, default behaviour will be used (closing the dialog and - calling host.action_manager) - - if a callback is provided, it will be used instead, so you'll have to manage - dialog closing or new xmlui to display, or other action (you can call - host.action_manager) - The callback will have data, callback_id and profile as arguments - """ - self.host = host - top = parsed_dom.documentElement - self.session_id = top.getAttribute("session_id") or None - self.submit_id = top.getAttribute("submit") or None - self.xmlui_title = title or top.getAttribute("title") or "" - self.hidden = {} - if flags is None: - flags = [] - self.flags = flags - self.callback = callback or self._default_cb - self.profile = profile - - @property - def user_action(self): - return "FROM_BACKEND" not in self.flags - - def _default_cb(self, data, cb_id, profile): - # TODO: when XMLUI updates will be managed, the _xmlui_close - # must be called only if there is no update - self._xmlui_close() - self.host.action_manager(data, profile=profile) - - def _is_attr_set(self, name, node): - """Return widget boolean attribute status - - @param name: name of the attribute (e.g. "read_only") - @param node: Node instance - @return (bool): True if widget's attribute is set (C.BOOL_TRUE) - """ - read_only = node.getAttribute(name) or C.BOOL_FALSE - return read_only.lower().strip() == C.BOOL_TRUE - - def _get_child_node(self, node, name): - """Return the first child node with the given name - - @param node: Node instance - @param name: name of the wanted node - - @return: The found element or None - """ - for child in node.childNodes: - if child.nodeName == name: - return child - return None - - def submit(self, data): - self._xmlui_close() - if self.submit_id is None: - raise ValueError("Can't submit is self.submit_id is not set") - if "session_id" in data: - raise ValueError( - "session_id must no be used in data, it is automaticaly filled with " - "self.session_id if present" - ) - if self.session_id is not None: - data["session_id"] = self.session_id - self._xmlui_launch_action(self.submit_id, data) - - def _xmlui_launch_action(self, action_id, data): - self.host.action_launch( - action_id, data, callback=self.callback, profile=self.profile - ) - - def _xmlui_close(self): - """Close the window/popup/... where the constructor XMLUI is - - this method must be overrided - """ - raise NotImplementedError - - -class ValueGetter(object): - """dict like object which return values of widgets""" - # FIXME: widget which can keep multiple values are not handled - - def __init__(self, widgets, attr="value"): - self.attr = attr - self.widgets = widgets - - def __getitem__(self, name): - return getattr(self.widgets[name], self.attr) - - def __getattr__(self, name): - return self.__getitem__(name) - - def keys(self): - return list(self.widgets.keys()) - - def items(self): - for name, widget in self.widgets.items(): - try: - value = widget.value - except AttributeError: - try: - value = list(widget.values) - except AttributeError: - continue - yield name, value - - -class XMLUIPanel(XMLUIBase): - """XMLUI Panel - - New frontends can inherit this class to easily implement XMLUI - @property widget_factory: factory to create frontend-specific widgets - @property dialog_factory: factory to create frontend-specific dialogs - """ - - widget_factory = None - - def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, - ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): - """ - - @param title(unicode, None): title of the - @property widgets(dict): widget name => widget map - @property widget_value(ValueGetter): retrieve widget value from it's name - """ - super(XMLUIPanel, self).__init__( - host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile - ) - self.ctrl_list = {} # input widget, used mainly for forms - self.widgets = {} #  allow to access any named widgets - self.widget_value = ValueGetter(self.widgets) - self._main_cont = None - if ignore is None: - ignore = [] - self._ignore = ignore - if whitelist is not None: - if ignore: - raise exceptions.InternalError( - "ignore and whitelist must not be used at the same time" - ) - self._whitelist = whitelist - else: - self._whitelist = None - self.construct_ui(parsed_dom) - - @staticmethod - def escape(name): - """Return escaped name for forms""" - return "%s%s" % (C.SAT_FORM_PREFIX, name) - - @property - def main_cont(self): - return self._main_cont - - @property - def values(self): - """Dict of all widgets values""" - return dict(self.widget_value.items()) - - @main_cont.setter - def main_cont(self, value): - if self._main_cont is not None: - raise ValueError(_("XMLUI can have only one main container")) - self._main_cont = value - - def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None): - """Recursively parse childNodes of an element - - @param _xmlui_parent: widget container with '_xmlui_append' method - @param current_node: element from which childs will be parsed - @param wanted: list of tag names that can be present in the childs to be SàT XMLUI - compliant - @param data(None, dict): additionnal data which are needed in some cases - """ - for node in current_node.childNodes: - if data is None: - data = {} - if wanted and not node.nodeName in wanted: - raise InvalidXMLUI("Unexpected node: [%s]" % node.nodeName) - - if node.nodeName == "container": - type_ = node.getAttribute("type") - if _xmlui_parent is self and type_ not in ("vertical", "tabs"): - # main container is not a VerticalContainer and we want one, - # so we create one to wrap it - _xmlui_parent = self.widget_factory.createVerticalContainer(self) - self.main_cont = _xmlui_parent - if type_ == "tabs": - cont = self.widget_factory.createTabsContainer(_xmlui_parent) - self._parse_childs(_xmlui_parent, node, ("tab",), {"tabs_cont": cont}) - elif type_ == "vertical": - cont = self.widget_factory.createVerticalContainer(_xmlui_parent) - self._parse_childs(cont, node, ("widget", "container")) - elif type_ == "pairs": - cont = self.widget_factory.createPairsContainer(_xmlui_parent) - self._parse_childs(cont, node, ("widget", "container")) - elif type_ == "label": - cont = self.widget_factory.createLabelContainer(_xmlui_parent) - self._parse_childs( - # FIXME: the "None" value for CURRENT_LABEL doesn't seem - # used or even useful, it should probably be removed - # and all "is not None" tests for it should be removed too - # to be checked for 0.8 - cont, node, ("widget", "container"), {CURRENT_LABEL: None} - ) - elif type_ == "advanced_list": - try: - columns = int(node.getAttribute("columns")) - except (TypeError, ValueError): - raise exceptions.DataError("Invalid columns") - selectable = node.getAttribute("selectable") or "no" - auto_index = node.getAttribute("auto_index") == C.BOOL_TRUE - data = {"index": 0} if auto_index else None - cont = self.widget_factory.createAdvancedListContainer( - _xmlui_parent, columns, selectable - ) - callback_id = node.getAttribute("callback") or None - if callback_id is not None: - if selectable == "no": - raise ValueError( - "can't have selectable=='no' and callback_id at the same time" - ) - cont._xmlui_callback_id = callback_id - cont._xmlui_on_select(self.on_adv_list_select) - - self._parse_childs(cont, node, ("row",), data) - else: - log.warning(_("Unknown container [%s], using default one") % type_) - cont = self.widget_factory.createVerticalContainer(_xmlui_parent) - self._parse_childs(cont, node, ("widget", "container")) - try: - xmluiAppend = _xmlui_parent._xmlui_append - except ( - AttributeError, - TypeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError - if _xmlui_parent is self: - self.main_cont = cont - else: - raise Exception( - _("Internal Error, container has not _xmlui_append method") - ) - else: - xmluiAppend(cont) - - elif node.nodeName == "tab": - name = node.getAttribute("name") - label = node.getAttribute("label") - selected = C.bool(node.getAttribute("selected") or C.BOOL_FALSE) - if not name or not "tabs_cont" in data: - raise InvalidXMLUI - if self.type == "param": - self._current_category = ( - name - ) # XXX: awful hack because params need category and we don't keep parent - tab_cont = data["tabs_cont"] - new_tab = tab_cont._xmlui_add_tab(label or name, selected) - self._parse_childs(new_tab, node, ("widget", "container")) - - elif node.nodeName == "row": - try: - index = str(data["index"]) - except KeyError: - index = node.getAttribute("index") or None - else: - data["index"] += 1 - _xmlui_parent._xmlui_add_row(index) - self._parse_childs(_xmlui_parent, node, ("widget", "container")) - - elif node.nodeName == "widget": - name = node.getAttribute("name") - if name and ( - name in self._ignore - or self._whitelist is not None - and name not in self._whitelist - ): - # current widget is ignored, but there may be already a label - if CURRENT_LABEL in data: - curr_label = data.pop(CURRENT_LABEL) - if curr_label is not None: - # if so, we remove it from parent - _xmlui_parent._xmlui_remove(curr_label) - continue - type_ = node.getAttribute("type") - value_elt = self._get_child_node(node, "value") - if value_elt is not None: - value = get_text(value_elt) - else: - value = ( - node.getAttribute("value") if node.hasAttribute("value") else "" - ) - if type_ == "empty": - ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent) - if CURRENT_LABEL in data: - data[CURRENT_LABEL] = None - elif type_ == "text": - ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value) - elif type_ == "label": - ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value) - data[CURRENT_LABEL] = ctrl - elif type_ == "hidden": - if name in self.hidden: - raise exceptions.ConflictError("Conflict on hidden value with " - "name {name}".format(name=name)) - self.hidden[name] = value - continue - elif type_ == "jid": - ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value) - elif type_ == "divider": - style = node.getAttribute("style") or "line" - ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style) - elif type_ == "string": - ctrl = self.widget_factory.createStringWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "jid_input": - ctrl = self.widget_factory.createJidInputWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "password": - ctrl = self.widget_factory.createPasswordWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "textbox": - ctrl = self.widget_factory.createTextBoxWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "xhtmlbox": - ctrl = self.widget_factory.createXHTMLBoxWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "bool": - ctrl = self.widget_factory.createBoolWidget( - _xmlui_parent, - value == C.BOOL_TRUE, - self._is_attr_set("read_only", node), - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "int": - ctrl = self.widget_factory.createIntWidget( - _xmlui_parent, value, self._is_attr_set("read_only", node) - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "list": - style = [] if node.getAttribute("multi") == "yes" else ["single"] - for attr in ("noselect", "extensible", "reducible", "inline"): - if node.getAttribute(attr) == "yes": - style.append(attr) - _options = [ - (option.getAttribute("value"), option.getAttribute("label")) - for option in node.getElementsByTagName("option") - ] - _selected = [ - option.getAttribute("value") - for option in node.getElementsByTagName("option") - if option.getAttribute("selected") == C.BOOL_TRUE - ] - ctrl = self.widget_factory.createListWidget( - _xmlui_parent, _options, _selected, style - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "jids_list": - style = [] - jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")] - ctrl = self.widget_factory.createJidsListWidget( - _xmlui_parent, jids, style - ) - self.ctrl_list[name] = {"type": type_, "control": ctrl} - elif type_ == "button": - callback_id = node.getAttribute("callback") - ctrl = self.widget_factory.createButtonWidget( - _xmlui_parent, value, self.on_button_press - ) - ctrl._xmlui_param_id = ( - callback_id, - [ - field.getAttribute("name") - for field in node.getElementsByTagName("field_back") - ], - ) - else: - log.error( - _("FIXME FIXME FIXME: widget type [%s] is not implemented") - % type_ - ) - raise NotImplementedError( - _("FIXME FIXME FIXME: type [%s] is not implemented") % type_ - ) - - if name: - self.widgets[name] = ctrl - - if self.type == "param" and type_ not in ("text", "button"): - try: - ctrl._xmlui_on_change(self.on_param_change) - ctrl._param_category = self._current_category - except ( - AttributeError, - TypeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead - # of an AttributeError - if not isinstance( - ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget) - ): - log.warning(_("No change listener on [%s]") % ctrl) - - elif type_ != "text": - callback = node.getAttribute("internal_callback") or None - if callback: - fields = [ - field.getAttribute("name") - for field in node.getElementsByTagName("internal_field") - ] - cb_data = self.get_internal_callback_data(callback, node) - ctrl._xmlui_param_internal = (callback, fields, cb_data) - if type_ == "button": - ctrl._xmlui_on_click(self.on_change_internal) - else: - ctrl._xmlui_on_change(self.on_change_internal) - - ctrl._xmlui_name = name - _xmlui_parent._xmlui_append(ctrl) - if CURRENT_LABEL in data and not isinstance(ctrl, LabelWidget): - curr_label = data.pop(CURRENT_LABEL) - if curr_label is not None: - # this key is set in LabelContainer, when present - # we can associate the label with the widget it is labelling - curr_label._xmlui_for_name = name - - else: - raise NotImplementedError(_("Unknown tag [%s]") % node.nodeName) - - def construct_ui(self, parsed_dom, post_treat=None): - """Actually construct the UI - - @param parsed_dom: main parsed dom - @param post_treat: frontend specific treatments to do once the UI is constructed - @return: constructed widget - """ - top = parsed_dom.documentElement - self.type = top.getAttribute("type") - if top.nodeName != "sat_xmlui" or not self.type in [ - "form", - "param", - "window", - "popup", - ]: - raise InvalidXMLUI - - if self.type == "param": - self.param_changed = set() - - self._parse_childs(self, parsed_dom.documentElement) - - if post_treat is not None: - post_treat() - - def _xmlui_set_param(self, name, value, category): - self.host.bridge.param_set(name, value, category, profile_key=self.profile) - - ##EVENTS## - - def on_param_change(self, ctrl): - """Called when type is param and a widget to save is modified - - @param ctrl: widget modified - """ - assert self.type == "param" - self.param_changed.add(ctrl) - - def on_adv_list_select(self, ctrl): - data = {} - widgets = ctrl._xmlui_get_selected_widgets() - for wid in widgets: - try: - name = self.escape(wid._xmlui_name) - value = wid._xmlui_get_value() - data[name] = value - except ( - AttributeError, - TypeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError - pass - idx = ctrl._xmlui_get_selected_index() - if idx is not None: - data["index"] = idx - callback_id = ctrl._xmlui_callback_id - if callback_id is None: - log.info(_("No callback_id found")) - return - self._xmlui_launch_action(callback_id, data) - - def on_button_press(self, button): - """Called when an XMLUI button is clicked - - Launch the action associated to the button - @param button: the button clicked - """ - callback_id, fields = button._xmlui_param_id - if not callback_id: # the button is probably bound to an internal action - return - data = {} - for field in fields: - escaped = self.escape(field) - ctrl = self.ctrl_list[field] - if isinstance(ctrl["control"], ListWidget): - data[escaped] = "\t".join(ctrl["control"]._xmlui_get_selected_values()) - else: - data[escaped] = ctrl["control"]._xmlui_get_value() - self._xmlui_launch_action(callback_id, data) - - def on_change_internal(self, ctrl): - """Called when a widget that has been bound to an internal callback is changed. - - This is used to perform some UI actions without communicating with the backend. - See sat.tools.xml_tools.Widget.set_internal_callback for more details. - @param ctrl: widget modified - """ - action, fields, data = ctrl._xmlui_param_internal - if action not in ("copy", "move", "groups_of_contact"): - raise NotImplementedError( - _("FIXME: XMLUI internal action [%s] is not implemented") % action - ) - - def copy_move(source, target): - """Depending of 'action' value, copy or move from source to target.""" - if isinstance(target, ListWidget): - if isinstance(source, ListWidget): - values = source._xmlui_get_selected_values() - else: - values = [source._xmlui_get_value()] - if action == "move": - source._xmlui_set_value("") - values = [value for value in values if value] - if values: - target._xmlui_add_values(values, select=True) - else: - if isinstance(source, ListWidget): - value = ", ".join(source._xmlui_get_selected_values()) - else: - value = source._xmlui_get_value() - if action == "move": - source._xmlui_set_value("") - target._xmlui_set_value(value) - - def groups_of_contact(source, target): - """Select in target the groups of the contact which is selected in source.""" - assert isinstance(source, ListWidget) - assert isinstance(target, ListWidget) - try: - contact_jid_s = source._xmlui_get_selected_values()[0] - except IndexError: - return - target._xmlui_select_values(data[contact_jid_s]) - pass - - source = None - for field in fields: - widget = self.ctrl_list[field]["control"] - if not source: - source = widget - continue - if action in ("copy", "move"): - copy_move(source, widget) - elif action == "groups_of_contact": - groups_of_contact(source, widget) - source = None - - def get_internal_callback_data(self, action, node): - """Retrieve from node the data needed to perform given action. - - @param action (string): a value from the one that can be passed to the - 'callback' parameter of sat.tools.xml_tools.Widget.set_internal_callback - @param node (DOM Element): the node of the widget that triggers the callback - """ - # TODO: it would be better to not have a specific way to retrieve - # data for each action, but instead to have a generic method to - # extract any kind of data structure from the 'internal_data' element. - - try: # data is stored in the first 'internal_data' element of the node - data_elts = node.getElementsByTagName("internal_data")[0].childNodes - except IndexError: - return None - data = {} - if ( - action == "groups_of_contact" - ): # return a dict(key: string, value: list[string]) - for elt in data_elts: - jid_s = elt.getAttribute("name") - data[jid_s] = [] - for value_elt in elt.childNodes: - data[jid_s].append(value_elt.getAttribute("name")) - return data - - def on_form_submitted(self, ignore=None): - """An XMLUI form has been submited - - call the submit action associated with this form - """ - selected_values = [] - for ctrl_name in self.ctrl_list: - escaped = self.escape(ctrl_name) - ctrl = self.ctrl_list[ctrl_name] - if isinstance(ctrl["control"], ListWidget): - selected_values.append( - (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) - ) - else: - selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) - data = dict(selected_values) - for key, value in self.hidden.items(): - data[self.escape(key)] = value - - if self.submit_id is not None: - self.submit(data) - else: - log.warning( - _("The form data is not sent back, the type is not managed properly") - ) - self._xmlui_close() - - def on_form_cancelled(self, *__): - """Called when a form is cancelled""" - log.debug(_("Cancelling form")) - if self.submit_id is not None: - data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} - self.submit(data) - else: - log.warning( - _("The form data is not sent back, the type is not managed properly") - ) - self._xmlui_close() - - def on_save_params(self, ignore=None): - """Params are saved, we send them to backend - - self.type must be param - """ - assert self.type == "param" - for ctrl in self.param_changed: - if isinstance(ctrl, ListWidget): - value = "\t".join(ctrl._xmlui_get_selected_values()) - else: - value = ctrl._xmlui_get_value() - param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1] - self._xmlui_set_param(param_name, value, ctrl._param_category) - - self._xmlui_close() - - def show(self, *args, **kwargs): - pass - - -class AIOXMLUIPanel(XMLUIPanel): - """Asyncio compatible version of XMLUIPanel""" - - async def on_form_submitted(self, ignore=None): - """An XMLUI form has been submited - - call the submit action associated with this form - """ - selected_values = [] - for ctrl_name in self.ctrl_list: - escaped = self.escape(ctrl_name) - ctrl = self.ctrl_list[ctrl_name] - if isinstance(ctrl["control"], ListWidget): - selected_values.append( - (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) - ) - else: - selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) - data = dict(selected_values) - for key, value in self.hidden.items(): - data[self.escape(key)] = value - - if self.submit_id is not None: - await self.submit(data) - else: - log.warning( - _("The form data is not sent back, the type is not managed properly") - ) - self._xmlui_close() - - async def on_form_cancelled(self, *__): - """Called when a form is cancelled""" - log.debug(_("Cancelling form")) - if self.submit_id is not None: - data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} - await self.submit(data) - else: - log.warning( - _("The form data is not sent back, the type is not managed properly") - ) - self._xmlui_close() - - async def submit(self, data): - self._xmlui_close() - if self.submit_id is None: - raise ValueError("Can't submit is self.submit_id is not set") - if "session_id" in data: - raise ValueError( - "session_id must no be used in data, it is automaticaly filled with " - "self.session_id if present" - ) - if self.session_id is not None: - data["session_id"] = self.session_id - await self._xmlui_launch_action(self.submit_id, data) - - async def _xmlui_launch_action(self, action_id, data): - await self.host.action_launch( - action_id, data, callback=self.callback, profile=self.profile - ) - - -class XMLUIDialog(XMLUIBase): - dialog_factory = None - - def __init__( - self, - host, - parsed_dom, - title=None, - flags=None, - callback=None, - ignore=None, - whitelist=None, - profile=C.PROF_KEY_NONE, - ): - super(XMLUIDialog, self).__init__( - host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile - ) - top = parsed_dom.documentElement - dlg_elt = self._get_child_node(top, "dialog") - if dlg_elt is None: - raise ValueError("Invalid XMLUI: no Dialog element found !") - dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE - try: - mess_elt = self._get_child_node(dlg_elt, C.XMLUI_DATA_MESS) - message = get_text(mess_elt) - except ( - TypeError, - AttributeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError - message = "" - level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO - - if dlg_type == C.XMLUI_DIALOG_MESSAGE: - self.dlg = self.dialog_factory.createMessageDialog( - self, self.xmlui_title, message, level - ) - elif dlg_type == C.XMLUI_DIALOG_NOTE: - self.dlg = self.dialog_factory.createNoteDialog( - self, self.xmlui_title, message, level - ) - elif dlg_type == C.XMLUI_DIALOG_CONFIRM: - try: - buttons_elt = self._get_child_node(dlg_elt, "buttons") - buttons_set = ( - buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT - ) - except ( - TypeError, - AttributeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError - buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT - self.dlg = self.dialog_factory.createConfirmDialog( - self, self.xmlui_title, message, level, buttons_set - ) - elif dlg_type == C.XMLUI_DIALOG_FILE: - try: - file_elt = self._get_child_node(dlg_elt, "file") - filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT - except ( - TypeError, - AttributeError, - ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError - filetype = C.XMLUI_DATA_FILETYPE_DEFAULT - self.dlg = self.dialog_factory.createFileDialog( - self, self.xmlui_title, message, level, filetype - ) - else: - raise ValueError("Unknown dialog type [%s]" % dlg_type) - - def show(self): - self.dlg._xmlui_show() - - def _xmlui_close(self): - self.dlg._xmlui_close() - - -def register_class(type_, class_): - """Register the class to use with the factory - - @param type_: one of: - CLASS_PANEL: classical XMLUI interface - CLASS_DIALOG: XMLUI dialog - @param class_: the class to use to instanciate given type - """ - # TODO: remove this method, as there are seme use cases where different XMLUI - # classes can be used in the same frontend, so a global value is not good - assert type_ in (CLASS_PANEL, CLASS_DIALOG) - log.warning("register_class for XMLUI is deprecated, please use partial with " - "xmlui.create and class_map instead") - if type_ in _class_map: - log.debug(_("XMLUI class already registered for {type_}, ignoring").format( - type_=type_)) - return - - _class_map[type_] = class_ - - -def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, - callback=None, ignore=None, whitelist=None, class_map=None, - profile=C.PROF_KEY_NONE): - """ - @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one - @param dom_free: method used to free the parsed DOM - @param ignore(list[unicode], None): name of widgets to ignore - widgets with name in this list and their label will be ignored - @param whitelist(list[unicode], None): name of widgets to keep - when not None, only widgets in this list and their label will be kept - mutually exclusive with ignore - """ - if class_map is None: - class_map = _class_map - if dom_parse is None: - from xml.dom import minidom - - dom_parse = lambda xml_data: minidom.parseString(xml_data.encode("utf-8")) - dom_free = lambda parsed_dom: parsed_dom.unlink() - else: - dom_parse = dom_parse - dom_free = dom_free or (lambda parsed_dom: None) - parsed_dom = dom_parse(xml_data) - top = parsed_dom.documentElement - ui_type = top.getAttribute("type") - try: - if ui_type != C.XMLUI_DIALOG: - cls = class_map[CLASS_PANEL] - else: - cls = class_map[CLASS_DIALOG] - except KeyError: - raise ClassNotRegistedError( - _("You must register classes with register_class before creating a XMLUI") - ) - - xmlui = cls( - host, - parsed_dom, - title=title, - flags=flags, - callback=callback, - ignore=ignore, - whitelist=whitelist, - profile=profile, - ) - dom_free(parsed_dom) - return xmlui diff -r 7c5654c54fed -r 26b7ed2817da setup.py --- a/setup.py Fri Jun 02 12:59:21 2023 +0200 +++ b/setup.py Fri Jun 02 14:12:38 2023 +0200 @@ -132,13 +132,13 @@ "sat = libervia.backend.core.launcher:Launcher.run", # CLI + aliases - "libervia-cli = sat_frontends.jp.base:LiberviaCli.run", - "li = sat_frontends.jp.base:LiberviaCli.run", - "jp = sat_frontends.jp.base:LiberviaCli.run", + "libervia-cli = libervia.frontends.jp.base:LiberviaCli.run", + "li = libervia.frontends.jp.base:LiberviaCli.run", + "jp = libervia.frontends.jp.base:LiberviaCli.run", # TUI + alias - "libervia-tui = sat_frontends.primitivus.base:PrimitivusApp.run", - "primitivus = sat_frontends.primitivus.base:PrimitivusApp.run", + "libervia-tui = libervia.frontends.primitivus.base:PrimitivusApp.run", + "primitivus = libervia.frontends.primitivus.base:PrimitivusApp.run", ], }, zip_safe=False,