view libervia/backend/bridge/dbus_bridge.py @ 4306:94e0968987cd

plugin XEP-0033: code modernisation, improve delivery, data validation: - Code has been rewritten using Pydantic models and `async` coroutines for data validation and cleaner element parsing/generation. - Delivery has been completely rewritten. It now works even if server doesn't support multicast, and send to local multicast service first. Delivering to local multicast service first is due to bad support of XEP-0033 in server (notably Prosody which has an incomplete implementation), and the current impossibility to detect if a sub-domain service handles fully multicast or only for local domains. This is a workaround to have a good balance between backward compatilibity and use of bandwith, and to make it work with the incoming email gateway implementation (the gateway will only deliver to entities of its own domain). - disco feature checking now uses `async` corountines. `host` implementation still use Deferred return values for compatibility with legacy code. rel 450
author Goffi <goffi@goffi.org>
date Thu, 26 Sep 2024 16:12:01 +0200
parents 3a550e9a2b55
children
line wrap: on
line source

#!/usr/bin/env python3

# Libervia 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 <http://www.gnu.org/licenses/>.

from types import MethodType
from functools import partialmethod
from twisted.internet import defer, reactor
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.core.exceptions import BridgeInitError
from libervia.backend.tools import config
from txdbus import client, objects, error
from txdbus.interface import DBusInterface, Method, Signal


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"


class ParseError(Exception):
    pass


class DBusException(Exception):
    pass


class MethodNotRegistered(DBusException):
    dbusErrorName = const_ERROR_PREFIX + ".MethodNotRegistered"


class GenericException(DBusException):
    def __init__(self, twisted_error):
        """

        @param twisted_error (Failure): instance of twisted Failure
        error message is used to store a repr of message and condition in a tuple,
        so it can be evaluated by the frontend bridge.
        """
        try:
            # twisted_error.value is a class
            class_ = twisted_error.value().__class__
        except TypeError:
            # twisted_error.value is an instance
            class_ = twisted_error.value.__class__
            data = twisted_error.getErrorMessage()
            try:
                data = (data, twisted_error.value.condition)
            except AttributeError:
                data = (data,)
        else:
            data = (str(twisted_error),)
        self.dbusErrorName = ".".join(
            (const_ERROR_PREFIX, class_.__module__, class_.__name__)
        )
        super(GenericException, self).__init__(repr(data))

    @classmethod
    def create_and_raise(cls, exc):
        raise cls(exc)


class DBusObject(objects.DBusObject):

    core_iface = DBusInterface(
        const_INT_PREFIX + const_CORE_SUFFIX,
        Method("action_launch", arguments="sss", returns="s"),
        Method("actions_get", arguments="s", returns="a(ssi)"),
        Method("config_get", arguments="ss", returns="s"),
        Method("connect", arguments="ssa{ss}", returns="b"),
        Method("contact_add", arguments="ss", returns=""),
        Method("contact_del", arguments="ss", returns=""),
        Method("contact_get", arguments="ss", returns="(a{ss}as)"),
        Method("contact_update", arguments="ssass", returns=""),
        Method("contacts_get", arguments="s", returns="a(sa{ss}as)"),
        Method("contacts_get_from_group", arguments="ss", returns="as"),
        Method("devices_infos_get", arguments="ss", returns="s"),
        Method(
            "disco_find_by_features",
            arguments="asa(ss)bbbbbs",
            returns="(a{sa(sss)}a{sa(sss)}a{sa(sss)})",
        ),
        Method("disco_infos", arguments="ssbs", returns="(asa(sss)a{sa(a{ss}as)})"),
        Method("disco_items", arguments="ssbs", returns="a(sss)"),
        Method("disconnect", arguments="s", returns=""),
        Method("encryption_namespace_get", arguments="s", returns="s"),
        Method("encryption_plugins_get", arguments="", returns="s"),
        Method("encryption_trust_ui_get", arguments="sss", returns="s"),
        Method("entities_data_get", arguments="asass", returns="a{sa{ss}}"),
        Method("entity_data_get", arguments="sass", returns="a{ss}"),
        Method("features_get", arguments="s", returns="a{sa{ss}}"),
        Method("history_get", arguments="ssiba{ss}s", returns="a(sdssa{ss}a{ss}ss)"),
        Method("image_check", arguments="s", returns="s"),
        Method("image_convert", arguments="ssss", returns="s"),
        Method("image_generate_preview", arguments="ss", returns="s"),
        Method("image_resize", arguments="sii", returns="s"),
        Method("init_pre_script", arguments="", returns=""),
        Method("is_connected", arguments="s", returns="b"),
        Method("main_resource_get", arguments="ss", returns="s"),
        Method("menu_help_get", arguments="ss", returns="s"),
        Method("menu_launch", arguments="sasa{ss}is", returns="a{ss}"),
        Method("menus_get", arguments="si", returns="a(ssasasa{ss})"),
        Method("message_encryption_get", arguments="ss", returns="s"),
        Method("message_encryption_start", arguments="ssbs", returns=""),
        Method("message_encryption_stop", arguments="ss", returns=""),
        Method("message_send", arguments="sa{ss}a{ss}sss", returns=""),
        Method("namespaces_get", arguments="", returns="a{ss}"),
        Method("notification_add", arguments="ssssbbsdss", returns=""),
        Method("notification_delete", arguments="sbs", returns=""),
        Method("notifications_expired_clean", arguments="ds", returns=""),
        Method("notifications_get", arguments="ss", returns="s"),
        Method("param_get_a", arguments="ssss", returns="s"),
        Method("param_get_a_async", arguments="sssis", returns="s"),
        Method("param_set", arguments="sssis", returns=""),
        Method("param_ui_get", arguments="isss", returns="s"),
        Method("params_categories_get", arguments="", returns="as"),
        Method("params_register_app", arguments="sis", returns=""),
        Method("params_template_load", arguments="s", returns="b"),
        Method("params_template_save", arguments="s", returns="b"),
        Method(
            "params_values_from_category_get_async", arguments="sisss", returns="a{ss}"
        ),
        Method("presence_set", arguments="ssa{ss}s", returns=""),
        Method("presence_statuses_get", arguments="s", returns="a{sa{s(sia{ss})}}"),
        Method("private_data_delete", arguments="sss", returns=""),
        Method("private_data_get", arguments="sss", returns="s"),
        Method("private_data_set", arguments="ssss", returns=""),
        Method("profile_create", arguments="sss", returns=""),
        Method("profile_delete_async", arguments="s", returns=""),
        Method("profile_is_session_started", arguments="s", returns="b"),
        Method("profile_name_get", arguments="s", returns="s"),
        Method("profile_set_default", arguments="s", returns=""),
        Method("profile_start_session", arguments="ss", returns="b"),
        Method("profiles_list_get", arguments="bb", returns="as"),
        Method("progress_get", arguments="ss", returns="a{ss}"),
        Method("progress_get_all", arguments="s", returns="a{sa{sa{ss}}}"),
        Method("progress_get_all_metadata", arguments="s", returns="a{sa{sa{ss}}}"),
        Method("ready_get", arguments="", returns=""),
        Method("roster_resync", arguments="s", returns=""),
        Method("session_infos_get", arguments="s", returns="a{ss}"),
        Method("sub_waiting_get", arguments="s", returns="a{ss}"),
        Method("subscription", arguments="sss", returns=""),
        Method("version_get", arguments="", returns="s"),
        Signal("_debug", "sa{ss}s"),
        Signal("action_new", "ssis"),
        Signal("connected", "ss"),
        Signal("contact_deleted", "ss"),
        Signal("contact_new", "sa{ss}ass"),
        Signal("disconnected", "s"),
        Signal("entity_data_updated", "ssss"),
        Signal("message_encryption_started", "sss"),
        Signal("message_encryption_stopped", "sa{ss}s"),
        Signal("message_new", "sdssa{ss}a{ss}sss"),
        Signal("message_update", "ssss"),
        Signal("notification_deleted", "ss"),
        Signal("notification_new", "sdssssbidss"),
        Signal("param_update", "ssss"),
        Signal("presence_update", "ssia{ss}s"),
        Signal("progress_error", "sss"),
        Signal("progress_finished", "sa{ss}s"),
        Signal("progress_started", "sa{ss}s"),
        Signal("subscribe", "sss"),
    )
    plugin_iface = DBusInterface(const_INT_PREFIX + const_PLUGIN_SUFFIX)

    dbusInterfaces = [core_iface, plugin_iface]

    def __init__(self, path):
        super().__init__(path)
        log.debug("Init DBusObject...")
        self.cb = {}

    def register_method(self, name, cb):
        self.cb[name] = cb

    def _callback(self, name, *args, **kwargs):
        """Call the callback if it exists, raise an exception else"""
        try:
            cb = self.cb[name]
        except KeyError:
            raise MethodNotRegistered
        else:
            d = defer.maybeDeferred(cb, *args, **kwargs)
            d.addErrback(GenericException.create_and_raise)
            return d

    def dbus_action_launch(self, callback_id, data, profile_key="@DEFAULT@"):
        return self._callback("action_launch", callback_id, data, profile_key)

    def dbus_actions_get(self, profile_key="@DEFAULT@"):
        return self._callback("actions_get", profile_key)

    def dbus_config_get(self, section, name):
        return self._callback("config_get", section, name)

    def dbus_connect(self, profile_key="@DEFAULT@", password="", options={}):
        return self._callback("connect", profile_key, password, options)

    def dbus_contact_add(self, entity_jid, profile_key="@DEFAULT@"):
        return self._callback("contact_add", entity_jid, profile_key)

    def dbus_contact_del(self, entity_jid, profile_key="@DEFAULT@"):
        return self._callback("contact_del", entity_jid, profile_key)

    def dbus_contact_get(self, arg_0, profile_key="@DEFAULT@"):
        return self._callback("contact_get", arg_0, profile_key)

    def dbus_contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"):
        return self._callback("contact_update", entity_jid, name, groups, profile_key)

    def dbus_contacts_get(self, profile_key="@DEFAULT@"):
        return self._callback("contacts_get", profile_key)

    def dbus_contacts_get_from_group(self, group, profile_key="@DEFAULT@"):
        return self._callback("contacts_get_from_group", group, profile_key)

    def dbus_devices_infos_get(self, bare_jid, profile_key):
        return self._callback("devices_infos_get", bare_jid, profile_key)

    def dbus_disco_find_by_features(
        self,
        namespaces,
        identities,
        bare_jid=False,
        service=True,
        roster=True,
        own_jid=True,
        local_device=False,
        profile_key="@DEFAULT@",
    ):
        return self._callback(
            "disco_find_by_features",
            namespaces,
            identities,
            bare_jid,
            service,
            roster,
            own_jid,
            local_device,
            profile_key,
        )

    def dbus_disco_infos(
        self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"
    ):
        return self._callback("disco_infos", entity_jid, node, use_cache, profile_key)

    def dbus_disco_items(
        self, entity_jid, node="", use_cache=True, profile_key="@DEFAULT@"
    ):
        return self._callback("disco_items", entity_jid, node, use_cache, profile_key)

    def dbus_disconnect(self, profile_key="@DEFAULT@"):
        return self._callback("disconnect", profile_key)

    def dbus_encryption_namespace_get(self, arg_0):
        return self._callback("encryption_namespace_get", arg_0)

    def dbus_encryption_plugins_get(
        self,
    ):
        return self._callback(
            "encryption_plugins_get",
        )

    def dbus_encryption_trust_ui_get(self, to_jid, namespace, profile_key):
        return self._callback("encryption_trust_ui_get", to_jid, namespace, profile_key)

    def dbus_entities_data_get(self, jids, keys, profile):
        return self._callback("entities_data_get", jids, keys, profile)

    def dbus_entity_data_get(self, jid, keys, profile):
        return self._callback("entity_data_get", jid, keys, profile)

    def dbus_features_get(self, profile_key):
        return self._callback("features_get", profile_key)

    def dbus_history_get(
        self, from_jid, to_jid, limit, between=True, filters="", profile="@NONE@"
    ):
        return self._callback(
            "history_get", from_jid, to_jid, limit, between, filters, profile
        )

    def dbus_image_check(self, arg_0):
        return self._callback("image_check", arg_0)

    def dbus_image_convert(self, source, dest, arg_2, extra):
        return self._callback("image_convert", source, dest, arg_2, extra)

    def dbus_image_generate_preview(self, image_path, profile_key):
        return self._callback("image_generate_preview", image_path, profile_key)

    def dbus_image_resize(self, image_path, width, height):
        return self._callback("image_resize", image_path, width, height)

    def dbus_init_pre_script(
        self,
    ):
        return self._callback(
            "init_pre_script",
        )

    def dbus_is_connected(self, profile_key="@DEFAULT@"):
        return self._callback("is_connected", profile_key)

    def dbus_main_resource_get(self, contact_jid, profile_key="@DEFAULT@"):
        return self._callback("main_resource_get", contact_jid, profile_key)

    def dbus_menu_help_get(self, menu_id, language):
        return self._callback("menu_help_get", menu_id, language)

    def dbus_menu_launch(self, menu_type, path, data, security_limit, profile_key):
        return self._callback(
            "menu_launch", menu_type, path, data, security_limit, profile_key
        )

    def dbus_menus_get(self, language, security_limit):
        return self._callback("menus_get", language, security_limit)

    def dbus_message_encryption_get(self, to_jid, profile_key):
        return self._callback("message_encryption_get", to_jid, profile_key)

    def dbus_message_encryption_start(
        self, to_jid, namespace="", replace=False, profile_key="@NONE@"
    ):
        return self._callback(
            "message_encryption_start", to_jid, namespace, replace, profile_key
        )

    def dbus_message_encryption_stop(self, to_jid, profile_key):
        return self._callback("message_encryption_stop", to_jid, profile_key)

    def dbus_message_send(
        self,
        to_jid,
        message,
        subject={},
        mess_type="auto",
        extra={},
        profile_key="@NONE@",
    ):
        return self._callback(
            "message_send", to_jid, message, subject, mess_type, extra, profile_key
        )

    def dbus_namespaces_get(
        self,
    ):
        return self._callback(
            "namespaces_get",
        )

    def dbus_notification_add(
        self,
        type_,
        body_plain,
        body_rich,
        title,
        is_global,
        requires_action,
        arg_6,
        priority,
        expire_at,
        extra,
    ):
        return self._callback(
            "notification_add",
            type_,
            body_plain,
            body_rich,
            title,
            is_global,
            requires_action,
            arg_6,
            priority,
            expire_at,
            extra,
        )

    def dbus_notification_delete(self, id_, is_global, profile_key):
        return self._callback("notification_delete", id_, is_global, profile_key)

    def dbus_notifications_expired_clean(self, limit_timestamp, profile_key):
        return self._callback("notifications_expired_clean", limit_timestamp, profile_key)

    def dbus_notifications_get(self, filters, profile_key):
        return self._callback("notifications_get", filters, profile_key)

    def dbus_param_get_a(
        self, name, category, attribute="value", profile_key="@DEFAULT@"
    ):
        return self._callback("param_get_a", name, category, attribute, profile_key)

    def dbus_param_get_a_async(
        self,
        name,
        category,
        attribute="value",
        security_limit=-1,
        profile_key="@DEFAULT@",
    ):
        return self._callback(
            "param_get_a_async", name, category, attribute, security_limit, profile_key
        )

    def dbus_param_set(
        self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"
    ):
        return self._callback(
            "param_set", name, value, category, security_limit, profile_key
        )

    def dbus_param_ui_get(
        self, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return self._callback("param_ui_get", security_limit, app, extra, profile_key)

    def dbus_params_categories_get(
        self,
    ):
        return self._callback(
            "params_categories_get",
        )

    def dbus_params_register_app(self, xml, security_limit=-1, app=""):
        return self._callback("params_register_app", xml, security_limit, app)

    def dbus_params_template_load(self, filename):
        return self._callback("params_template_load", filename)

    def dbus_params_template_save(self, filename):
        return self._callback("params_template_save", filename)

    def dbus_params_values_from_category_get_async(
        self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"
    ):
        return self._callback(
            "params_values_from_category_get_async",
            category,
            security_limit,
            app,
            extra,
            profile_key,
        )

    def dbus_presence_set(self, to_jid="", show="", statuses={}, profile_key="@DEFAULT@"):
        return self._callback("presence_set", to_jid, show, statuses, profile_key)

    def dbus_presence_statuses_get(self, profile_key="@DEFAULT@"):
        return self._callback("presence_statuses_get", profile_key)

    def dbus_private_data_delete(self, namespace, key, arg_2):
        return self._callback("private_data_delete", namespace, key, arg_2)

    def dbus_private_data_get(self, namespace, key, profile_key):
        return self._callback("private_data_get", namespace, key, profile_key)

    def dbus_private_data_set(self, namespace, key, data, profile_key):
        return self._callback("private_data_set", namespace, key, data, profile_key)

    def dbus_profile_create(self, profile, password="", component=""):
        return self._callback("profile_create", profile, password, component)

    def dbus_profile_delete_async(self, profile):
        return self._callback("profile_delete_async", profile)

    def dbus_profile_is_session_started(self, profile_key="@DEFAULT@"):
        return self._callback("profile_is_session_started", profile_key)

    def dbus_profile_name_get(self, profile_key="@DEFAULT@"):
        return self._callback("profile_name_get", profile_key)

    def dbus_profile_set_default(self, profile):
        return self._callback("profile_set_default", profile)

    def dbus_profile_start_session(self, password="", profile_key="@DEFAULT@"):
        return self._callback("profile_start_session", password, profile_key)

    def dbus_profiles_list_get(self, clients=True, components=False):
        return self._callback("profiles_list_get", clients, components)

    def dbus_progress_get(self, id, profile):
        return self._callback("progress_get", id, profile)

    def dbus_progress_get_all(self, profile):
        return self._callback("progress_get_all", profile)

    def dbus_progress_get_all_metadata(self, profile):
        return self._callback("progress_get_all_metadata", profile)

    def dbus_ready_get(
        self,
    ):
        return self._callback(
            "ready_get",
        )

    def dbus_roster_resync(self, profile_key="@DEFAULT@"):
        return self._callback("roster_resync", profile_key)

    def dbus_session_infos_get(self, profile_key):
        return self._callback("session_infos_get", profile_key)

    def dbus_sub_waiting_get(self, profile_key="@DEFAULT@"):
        return self._callback("sub_waiting_get", profile_key)

    def dbus_subscription(self, sub_type, entity, profile_key="@DEFAULT@"):
        return self._callback("subscription", sub_type, entity, profile_key)

    def dbus_version_get(
        self,
    ):
        return self._callback(
            "version_get",
        )


class bridge:

    def __init__(self):
        log.info("Init DBus...")
        self._obj = DBusObject(const_OBJ_PATH)

    async def post_init(self):
        try:
            conn = await client.connect(reactor)
        except error.DBusException as e:
            if e.errName == "org.freedesktop.DBus.Error.NotSupported":
                log.error(
                    _(
                        "D-Bus is not launched, please see README to see instructions on "
                        "how to launch it"
                    )
                )
            raise BridgeInitError(str(e))

        conn.exportObject(self._obj)
        await conn.requestBusName(const_INT_PREFIX)

    def _debug(self, action, params, profile):
        self._obj.emitSignal("_debug", action, params, profile)

    def action_new(self, action_data, id, security_limit, profile):
        self._obj.emitSignal("action_new", action_data, id, security_limit, profile)

    def connected(self, jid_s, profile):
        self._obj.emitSignal("connected", jid_s, profile)

    def contact_deleted(self, entity_jid, profile):
        self._obj.emitSignal("contact_deleted", entity_jid, profile)

    def contact_new(self, contact_jid, attributes, groups, profile):
        self._obj.emitSignal("contact_new", contact_jid, attributes, groups, profile)

    def disconnected(self, profile):
        self._obj.emitSignal("disconnected", profile)

    def entity_data_updated(self, jid, name, value, profile):
        self._obj.emitSignal("entity_data_updated", jid, name, value, profile)

    def message_encryption_started(self, to_jid, encryption_data, profile_key):
        self._obj.emitSignal(
            "message_encryption_started", to_jid, encryption_data, profile_key
        )

    def message_encryption_stopped(self, to_jid, encryption_data, profile_key):
        self._obj.emitSignal(
            "message_encryption_stopped", to_jid, encryption_data, profile_key
        )

    def message_new(
        self,
        uid,
        timestamp,
        from_jid,
        to_jid,
        message,
        subject,
        mess_type,
        extra,
        profile,
    ):
        self._obj.emitSignal(
            "message_new",
            uid,
            timestamp,
            from_jid,
            to_jid,
            message,
            subject,
            mess_type,
            extra,
            profile,
        )

    def message_update(self, uid, message_type, message_data, profile):
        self._obj.emitSignal("message_update", uid, message_type, message_data, profile)

    def notification_deleted(self, id, profile):
        self._obj.emitSignal("notification_deleted", id, profile)

    def notification_new(
        self,
        id,
        timestamp,
        type,
        body_plain,
        body_rich,
        title,
        requires_action,
        priority,
        expire_at,
        extra,
        profile,
    ):
        self._obj.emitSignal(
            "notification_new",
            id,
            timestamp,
            type,
            body_plain,
            body_rich,
            title,
            requires_action,
            priority,
            expire_at,
            extra,
            profile,
        )

    def param_update(self, name, value, category, profile):
        self._obj.emitSignal("param_update", name, value, category, profile)

    def presence_update(self, entity_jid, show, priority, statuses, profile):
        self._obj.emitSignal(
            "presence_update", entity_jid, show, priority, statuses, profile
        )

    def progress_error(self, id, error, profile):
        self._obj.emitSignal("progress_error", id, error, profile)

    def progress_finished(self, id, metadata, profile):
        self._obj.emitSignal("progress_finished", id, metadata, profile)

    def progress_started(self, id, metadata, profile):
        self._obj.emitSignal("progress_started", id, metadata, profile)

    def subscribe(self, sub_type, entity_jid, profile):
        self._obj.emitSignal("subscribe", sub_type, entity_jid, profile)

    def register_method(self, name, callback):
        log.debug(f"registering DBus bridge method [{name}]")
        self._obj.register_method(name, callback)

    def emit_signal(self, name, *args):
        self._obj.emitSignal(name, *args)

    def add_method(
        self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}
    ):
        """Dynamically add a method to D-Bus bridge"""
        # FIXME: doc parameter is kept only temporary, the time to remove it from calls
        log.debug(f"Adding method {name!r} to D-Bus bridge")
        self._obj.plugin_iface.addMethod(
            Method(name, arguments=in_sign, returns=out_sign)
        )

        # we have to create a method here instead of using partialmethod, because txdbus
        # uses __func__ which doesn't work with partialmethod
        def caller(self_, *args, **kwargs):
            return self_._callback(name, *args, **kwargs)

        setattr(self._obj, f"dbus_{name}", MethodType(caller, self._obj))
        self.register_method(name, method)

    def add_signal(self, name, int_suffix, signature, doc={}):
        """Dynamically add a signal to D-Bus bridge"""
        log.debug(f"Adding signal {name!r} to D-Bus bridge")
        self._obj.plugin_iface.addSignal(Signal(name, signature))
        setattr(bridge, name, partialmethod(bridge.emit_signal, name))