view sat/bridge/dbus_bridge.py @ 3728:b15644cae50d

component AP gateway: JID/node ⟺ AP outbox conversion: - convert a combination of JID and optional pubsub node to AP actor handle (see `getJIDAndNode` for details) and vice versa - the gateway now provides a Pubsub service - retrieve pubsub node and convert it to AP collection, AP pagination is converted to RSM - do the opposite: convert AP collection to pubsub and handle RSM request. Due to ActivityStream collection pagination limitations, some RSM request produce inefficient requests, but caching should be used most of the time in the future and avoid the problem. - set specific name to HTTP Server - new `local_only` setting (`True` by default) to indicate if the gateway can request or not XMPP Pubsub nodes from other servers - disco info now specifies important features such as Pubsub RSM, and nodes metadata ticket 363
author Goffi <goffi@goffi.org>
date Tue, 25 Jan 2022 17:54:06 +0100
parents 60d3861e5996
children 524856bd7b19
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 sat.core.i18n import _
from sat.core.log import getLogger
from sat.core.exceptions import BridgeInitError
from sat.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.getConfig(
    config.parseMainConf(),
    "",
    "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('actionsGet', arguments='s', returns='a(a{ss}si)'),
        Method('addContact', arguments='ss', returns=''),
        Method('asyncDeleteProfile', arguments='s', returns=''),
        Method('asyncGetParamA', arguments='sssis', returns='s'),
        Method('asyncGetParamsValuesFromCategory', arguments='sisss', returns='a{ss}'),
        Method('connect', arguments='ssa{ss}', returns='b'),
        Method('contactGet', arguments='ss', returns='(a{ss}as)'),
        Method('delContact', arguments='ss', returns=''),
        Method('devicesInfosGet', arguments='ss', returns='s'),
        Method('discoFindByFeatures', arguments='asa(ss)bbbbbs', returns='(a{sa(sss)}a{sa(sss)}a{sa(sss)})'),
        Method('discoInfos', arguments='ssbs', returns='(asa(sss)a{sa(a{ss}as)})'),
        Method('discoItems', arguments='ssbs', returns='a(sss)'),
        Method('disconnect', arguments='s', returns=''),
        Method('encryptionNamespaceGet', arguments='s', returns='s'),
        Method('encryptionPluginsGet', arguments='', returns='s'),
        Method('encryptionTrustUIGet', arguments='sss', returns='s'),
        Method('getConfig', arguments='ss', returns='s'),
        Method('getContacts', arguments='s', returns='a(sa{ss}as)'),
        Method('getContactsFromGroup', arguments='ss', returns='as'),
        Method('getEntitiesData', arguments='asass', returns='a{sa{ss}}'),
        Method('getEntityData', arguments='sass', returns='a{ss}'),
        Method('getFeatures', arguments='s', returns='a{sa{ss}}'),
        Method('getMainResource', arguments='ss', returns='s'),
        Method('getParamA', arguments='ssss', returns='s'),
        Method('getParamsCategories', arguments='', returns='as'),
        Method('getParamsUI', arguments='isss', returns='s'),
        Method('getPresenceStatuses', arguments='s', returns='a{sa{s(sia{ss})}}'),
        Method('getReady', arguments='', returns=''),
        Method('getVersion', arguments='', returns='s'),
        Method('getWaitingSub', arguments='s', returns='a{ss}'),
        Method('historyGet', arguments='ssiba{ss}s', returns='a(sdssa{ss}a{ss}ss)'),
        Method('imageCheck', arguments='s', returns='s'),
        Method('imageConvert', arguments='ssss', returns='s'),
        Method('imageGeneratePreview', arguments='ss', returns='s'),
        Method('imageResize', arguments='sii', returns='s'),
        Method('isConnected', arguments='s', returns='b'),
        Method('launchAction', arguments='sa{ss}s', returns='a{ss}'),
        Method('loadParamsTemplate', arguments='s', returns='b'),
        Method('menuHelpGet', arguments='ss', returns='s'),
        Method('menuLaunch', arguments='sasa{ss}is', returns='a{ss}'),
        Method('menusGet', arguments='si', returns='a(ssasasa{ss})'),
        Method('messageEncryptionGet', arguments='ss', returns='s'),
        Method('messageEncryptionStart', arguments='ssbs', returns=''),
        Method('messageEncryptionStop', arguments='ss', returns=''),
        Method('messageSend', arguments='sa{ss}a{ss}sss', returns=''),
        Method('namespacesGet', arguments='', returns='a{ss}'),
        Method('paramsRegisterApp', arguments='sis', returns=''),
        Method('privateDataDelete', arguments='sss', returns=''),
        Method('privateDataGet', arguments='sss', returns='s'),
        Method('privateDataSet', arguments='ssss', returns=''),
        Method('profileCreate', arguments='sss', returns=''),
        Method('profileIsSessionStarted', arguments='s', returns='b'),
        Method('profileNameGet', arguments='s', returns='s'),
        Method('profileSetDefault', arguments='s', returns=''),
        Method('profileStartSession', arguments='ss', returns='b'),
        Method('profilesListGet', arguments='bb', returns='as'),
        Method('progressGet', arguments='ss', returns='a{ss}'),
        Method('progressGetAll', arguments='s', returns='a{sa{sa{ss}}}'),
        Method('progressGetAllMetadata', arguments='s', returns='a{sa{sa{ss}}}'),
        Method('rosterResync', arguments='s', returns=''),
        Method('saveParamsTemplate', arguments='s', returns='b'),
        Method('sessionInfosGet', arguments='s', returns='a{ss}'),
        Method('setParam', arguments='sssis', returns=''),
        Method('setPresence', arguments='ssa{ss}s', returns=''),
        Method('subscription', arguments='sss', returns=''),
        Method('updateContact', arguments='ssass', returns=''),
        Signal('_debug', 'sa{ss}s'),
        Signal('actionNew', 'a{ss}sis'),
        Signal('connected', 'ss'),
        Signal('contactDeleted', 'ss'),
        Signal('disconnected', 's'),
        Signal('entityDataUpdated', 'ssss'),
        Signal('messageEncryptionStarted', 'sss'),
        Signal('messageEncryptionStopped', 'sa{ss}s'),
        Signal('messageNew', 'sdssa{ss}a{ss}sss'),
        Signal('newContact', 'sa{ss}ass'),
        Signal('paramUpdate', 'ssss'),
        Signal('presenceUpdate', 'ssia{ss}s'),
        Signal('progressError', 'sss'),
        Signal('progressFinished', 'sa{ss}s'),
        Signal('progressStarted', '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_actionsGet(self, profile_key="@DEFAULT@"):
        return self._callback("actionsGet", profile_key)

    def dbus_addContact(self, entity_jid, profile_key="@DEFAULT@"):
        return self._callback("addContact", entity_jid, profile_key)

    def dbus_asyncDeleteProfile(self, profile):
        return self._callback("asyncDeleteProfile", profile)

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

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

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

    def dbus_contactGet(self, arg_0, profile_key="@DEFAULT@"):
        return self._callback("contactGet", arg_0, profile_key)

    def dbus_delContact(self, entity_jid, profile_key="@DEFAULT@"):
        return self._callback("delContact", entity_jid, profile_key)

    def dbus_devicesInfosGet(self, bare_jid, profile_key):
        return self._callback("devicesInfosGet", bare_jid, profile_key)

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

    def dbus_discoInfos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"):
        return self._callback("discoInfos", entity_jid, node, use_cache, profile_key)

    def dbus_discoItems(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"):
        return self._callback("discoItems", entity_jid, node, use_cache, profile_key)

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

    def dbus_encryptionNamespaceGet(self, arg_0):
        return self._callback("encryptionNamespaceGet", arg_0)

    def dbus_encryptionPluginsGet(self, ):
        return self._callback("encryptionPluginsGet", )

    def dbus_encryptionTrustUIGet(self, to_jid, namespace, profile_key):
        return self._callback("encryptionTrustUIGet", to_jid, namespace, profile_key)

    def dbus_getConfig(self, section, name):
        return self._callback("getConfig", section, name)

    def dbus_getContacts(self, profile_key="@DEFAULT@"):
        return self._callback("getContacts", profile_key)

    def dbus_getContactsFromGroup(self, group, profile_key="@DEFAULT@"):
        return self._callback("getContactsFromGroup", group, profile_key)

    def dbus_getEntitiesData(self, jids, keys, profile):
        return self._callback("getEntitiesData", jids, keys, profile)

    def dbus_getEntityData(self, jid, keys, profile):
        return self._callback("getEntityData", jid, keys, profile)

    def dbus_getFeatures(self, profile_key):
        return self._callback("getFeatures", profile_key)

    def dbus_getMainResource(self, contact_jid, profile_key="@DEFAULT@"):
        return self._callback("getMainResource", contact_jid, profile_key)

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

    def dbus_getParamsCategories(self, ):
        return self._callback("getParamsCategories", )

    def dbus_getParamsUI(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"):
        return self._callback("getParamsUI", security_limit, app, extra, profile_key)

    def dbus_getPresenceStatuses(self, profile_key="@DEFAULT@"):
        return self._callback("getPresenceStatuses", profile_key)

    def dbus_getReady(self, ):
        return self._callback("getReady", )

    def dbus_getVersion(self, ):
        return self._callback("getVersion", )

    def dbus_getWaitingSub(self, profile_key="@DEFAULT@"):
        return self._callback("getWaitingSub", profile_key)

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

    def dbus_imageCheck(self, arg_0):
        return self._callback("imageCheck", arg_0)

    def dbus_imageConvert(self, source, dest, arg_2, extra):
        return self._callback("imageConvert", source, dest, arg_2, extra)

    def dbus_imageGeneratePreview(self, image_path, profile_key):
        return self._callback("imageGeneratePreview", image_path, profile_key)

    def dbus_imageResize(self, image_path, width, height):
        return self._callback("imageResize", image_path, width, height)

    def dbus_isConnected(self, profile_key="@DEFAULT@"):
        return self._callback("isConnected", profile_key)

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

    def dbus_loadParamsTemplate(self, filename):
        return self._callback("loadParamsTemplate", filename)

    def dbus_menuHelpGet(self, menu_id, language):
        return self._callback("menuHelpGet", menu_id, language)

    def dbus_menuLaunch(self, menu_type, path, data, security_limit, profile_key):
        return self._callback("menuLaunch", menu_type, path, data, security_limit, profile_key)

    def dbus_menusGet(self, language, security_limit):
        return self._callback("menusGet", language, security_limit)

    def dbus_messageEncryptionGet(self, to_jid, profile_key):
        return self._callback("messageEncryptionGet", to_jid, profile_key)

    def dbus_messageEncryptionStart(self, to_jid, namespace='', replace=False, profile_key="@NONE@"):
        return self._callback("messageEncryptionStart", to_jid, namespace, replace, profile_key)

    def dbus_messageEncryptionStop(self, to_jid, profile_key):
        return self._callback("messageEncryptionStop", to_jid, profile_key)

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

    def dbus_namespacesGet(self, ):
        return self._callback("namespacesGet", )

    def dbus_paramsRegisterApp(self, xml, security_limit=-1, app=''):
        return self._callback("paramsRegisterApp", xml, security_limit, app)

    def dbus_privateDataDelete(self, namespace, key, arg_2):
        return self._callback("privateDataDelete", namespace, key, arg_2)

    def dbus_privateDataGet(self, namespace, key, profile_key):
        return self._callback("privateDataGet", namespace, key, profile_key)

    def dbus_privateDataSet(self, namespace, key, data, profile_key):
        return self._callback("privateDataSet", namespace, key, data, profile_key)

    def dbus_profileCreate(self, profile, password='', component=''):
        return self._callback("profileCreate", profile, password, component)

    def dbus_profileIsSessionStarted(self, profile_key="@DEFAULT@"):
        return self._callback("profileIsSessionStarted", profile_key)

    def dbus_profileNameGet(self, profile_key="@DEFAULT@"):
        return self._callback("profileNameGet", profile_key)

    def dbus_profileSetDefault(self, profile):
        return self._callback("profileSetDefault", profile)

    def dbus_profileStartSession(self, password='', profile_key="@DEFAULT@"):
        return self._callback("profileStartSession", password, profile_key)

    def dbus_profilesListGet(self, clients=True, components=False):
        return self._callback("profilesListGet", clients, components)

    def dbus_progressGet(self, id, profile):
        return self._callback("progressGet", id, profile)

    def dbus_progressGetAll(self, profile):
        return self._callback("progressGetAll", profile)

    def dbus_progressGetAllMetadata(self, profile):
        return self._callback("progressGetAllMetadata", profile)

    def dbus_rosterResync(self, profile_key="@DEFAULT@"):
        return self._callback("rosterResync", profile_key)

    def dbus_saveParamsTemplate(self, filename):
        return self._callback("saveParamsTemplate", filename)

    def dbus_sessionInfosGet(self, profile_key):
        return self._callback("sessionInfosGet", profile_key)

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

    def dbus_setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"):
        return self._callback("setPresence", to_jid, show, statuses, profile_key)

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

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


class Bridge:

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

    async def postInit(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 actionNew(self, action_data, id, security_limit, profile):
        self._obj.emitSignal("actionNew", action_data, id, security_limit, profile)

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

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

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

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

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

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

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

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

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

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

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

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

    def progressStarted(self, id, metadata, profile):
        self._obj.emitSignal("progressStarted", 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 emitSignal(self, name, *args):
        self._obj.emitSignal(name, *args)

    def addMethod(
            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 addSignal(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.emitSignal, name))