view sat/bridge/dbus_bridge.py @ 3888:aa7197b67c26

component AP gateway: AP <=> XMPP reactions conversions: - Pubsub Attachments plugin has been renamed to XEP-0470 following publication - XEP-0470 has been updated to follow 0.2 changes - AP reactions (as implemented in Pleroma) are converted to XEP-0470 - XEP-0470 events are converted to AP reactions (again, using "EmojiReact" from Pleroma) - AP activities related to attachments (like/reactions) are cached in Libervia because it's not possible to retrieve them from Pleroma instances once they have been emitted (doing an HTTP get on their ID returns a 404). For now those cache are not flushed, this should be improved in the future. - `sharedInbox` is used when available. Pleroma returns a 500 HTTP error when ``to`` or ``cc`` are used in a direct inbox. - reactions and like are not currently used for direct messages, because they can't be emitted from Pleroma in this case, thus there is no point in implementing them for the moment. rel 371
author Goffi <goffi@goffi.org>
date Wed, 31 Aug 2022 17:07:03 +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))