view sat/bridge/dbus_bridge.py @ 3757:5bda9d2e8b35

plugin XEP-0060: use serialised options in `psSubscribe` + triggers update
author Goffi <goffi@goffi.org>
date Fri, 13 May 2022 18:29:42 +0200
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))