Mercurial > libervia-backend
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))