Mercurial > libervia-backend
view sat/plugins/plugin_adhoc_dbus.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 | 7550ae9cfbac |
children | 524856bd7b19 |
line wrap: on
line source
#!/usr/bin/env python3 # SAT plugin for adding D-Bus to Ad-Hoc Commands # 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 sat.core.i18n import D_, _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.internet import defer from twisted.words.protocols.jabber import jid from wokkel import data_form try: from lxml import etree except ImportError: etree = None log.warning("Missing module lxml, please download/install it from http://lxml.de/ ." "Auto D-Bus discovery will be disabled") from collections import OrderedDict import os.path import uuid try: import dbus from dbus.mainloop.glib import DBusGMainLoop except ImportError: dbus = None log.warning("Missing module dbus, please download/install it, " "auto D-Bus discovery will be disabled") else: DBusGMainLoop(set_as_default=True) NS_MEDIA_PLAYER = "org.libervia.mediaplayer" FD_NAME = "org.freedesktop.DBus" FD_PATH = "/org/freedekstop/DBus" INTROSPECT_IFACE = "org.freedesktop.DBus.Introspectable" MPRIS_PREFIX = "org.mpris.MediaPlayer2" CMD_GO_BACK = "GoBack" CMD_GO_FWD = "GoFW" SEEK_OFFSET = 5 * 1000 * 1000 MPRIS_COMMANDS = ["org.mpris.MediaPlayer2.Player." + cmd for cmd in ( "Previous", CMD_GO_BACK, "PlayPause", CMD_GO_FWD, "Next")] MPRIS_PATH = "/org/mpris/MediaPlayer2" MPRIS_PROPERTIES = OrderedDict(( ("org.mpris.MediaPlayer2", ( "Identity", )), ("org.mpris.MediaPlayer2.Player", ( "Metadata", "PlaybackStatus", "Volume", )), )) MPRIS_METADATA_KEY = "Metadata" MPRIS_METADATA_MAP = OrderedDict(( ("xesam:title", "Title"), )) INTROSPECT_METHOD = "Introspect" IGNORED_IFACES_START = ( "org.freedesktop", "org.qtproject", "org.kde.KMainWindow", ) # commands in interface starting with these values will be ignored FLAG_LOOP = "LOOP" PLUGIN_INFO = { C.PI_NAME: "Ad-Hoc Commands - D-Bus", C.PI_IMPORT_NAME: "AD_HOC_DBUS", C.PI_TYPE: "Misc", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0050"], C.PI_MAIN: "AdHocDBus", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Add D-Bus management to Ad-Hoc commands"""), } class AdHocDBus(object): def __init__(self, host): log.info(_("plugin Ad-Hoc D-Bus initialization")) self.host = host if etree is not None: host.bridge.addMethod( "adHocDBusAddAuto", ".plugin", in_sign="sasasasasasass", out_sign="(sa(sss))", method=self._adHocDBusAddAuto, async_=True, ) host.bridge.addMethod( "adHocRemotesGet", ".plugin", in_sign="s", out_sign="a(sss)", method=self._adHocRemotesGet, async_=True, ) self._c = host.plugins["XEP-0050"] host.registerNamespace("mediaplayer", NS_MEDIA_PLAYER) if dbus is not None: self.session_bus = dbus.SessionBus() self.fd_object = self.session_bus.get_object( FD_NAME, FD_PATH, introspect=False) def profileConnected(self, client): if dbus is not None: self._c.addAdHocCommand( client, self.localMediaCb, D_("Media Players"), node=NS_MEDIA_PLAYER, timeout=60*60*6 # 6 hours timeout, to avoid breaking remote # in the middle of a movie ) def _DBusAsyncCall(self, proxy, method, *args, **kwargs): """ Call a DBus method asynchronously and return a deferred @param proxy: DBus object proxy, as returner by get_object @param method: name of the method to call @param args: will be transmitted to the method @param kwargs: will be transmetted to the method, except for the following poped values: - interface: name of the interface to use @return: a deferred """ d = defer.Deferred() interface = kwargs.pop("interface", None) kwargs["reply_handler"] = lambda ret=None: d.callback(ret) kwargs["error_handler"] = d.errback proxy.get_dbus_method(method, dbus_interface=interface)(*args, **kwargs) return d def _DBusGetProperty(self, proxy, interface, name): return self._DBusAsyncCall( proxy, "Get", interface, name, interface="org.freedesktop.DBus.Properties") def _DBusListNames(self): return self._DBusAsyncCall(self.fd_object, "ListNames") def _DBusIntrospect(self, proxy): return self._DBusAsyncCall(proxy, INTROSPECT_METHOD, interface=INTROSPECT_IFACE) def _acceptMethod(self, method): """ Return True if we accept the method for a command @param method: etree.Element @return: True if the method is acceptable """ if method.xpath( "arg[@direction='in']" ): # we don't accept method with argument for the moment return False return True @defer.inlineCallbacks def _introspect(self, methods, bus_name, proxy): log.debug("introspecting path [%s]" % proxy.object_path) introspect_xml = yield self._DBusIntrospect(proxy) el = etree.fromstring(introspect_xml) for node in el.iterchildren("node", "interface"): if node.tag == "node": new_path = os.path.join(proxy.object_path, node.get("name")) new_proxy = self.session_bus.get_object( bus_name, new_path, introspect=False ) yield self._introspect(methods, bus_name, new_proxy) elif node.tag == "interface": name = node.get("name") if any(name.startswith(ignored) for ignored in IGNORED_IFACES_START): log.debug("interface [%s] is ignored" % name) continue log.debug("introspecting interface [%s]" % name) for method in node.iterchildren("method"): if self._acceptMethod(method): method_name = method.get("name") log.debug("method accepted: [%s]" % method_name) methods.add((proxy.object_path, name, method_name)) def _adHocDBusAddAuto(self, prog_name, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, flags, profile_key): client = self.host.getClient(profile_key) return self.adHocDBusAddAuto( client, prog_name, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, flags) @defer.inlineCallbacks def adHocDBusAddAuto(self, client, prog_name, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None, flags=None): bus_names = yield self._DBusListNames() bus_names = [bus_name for bus_name in bus_names if "." + prog_name in bus_name] if not bus_names: log.info("Can't find any bus for [%s]" % prog_name) defer.returnValue(("", [])) bus_names.sort() for bus_name in bus_names: if bus_name.endswith(prog_name): break log.info("bus name found: [%s]" % bus_name) proxy = self.session_bus.get_object(bus_name, "/", introspect=False) methods = set() yield self._introspect(methods, bus_name, proxy) if methods: self._addCommand( client, prog_name, bus_name, methods, allowed_jids=allowed_jids, allowed_groups=allowed_groups, allowed_magics=allowed_magics, forbidden_jids=forbidden_jids, forbidden_groups=forbidden_groups, flags=flags, ) defer.returnValue((str(bus_name), methods)) def _addCommand(self, client, adhoc_name, bus_name, methods, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None, flags=None): if flags is None: flags = set() def DBusCallback(client, command_elt, session_data, action, node): actions = session_data.setdefault("actions", []) names_map = session_data.setdefault("names_map", {}) actions.append(action) if len(actions) == 1: # it's our first request, we ask the desired new status status = self._c.STATUS.EXECUTING form = data_form.Form("form", title=_("Command selection")) options = [] for path, iface, command in methods: label = command.rsplit(".", 1)[-1] name = str(uuid.uuid4()) names_map[name] = (path, iface, command) options.append(data_form.Option(name, label)) field = data_form.Field( "list-single", "command", options=options, required=True ) form.addField(field) payload = form.toElement() note = None elif len(actions) == 2: # we should have the answer here try: x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x")) answer_form = data_form.Form.fromElement(x_elt) command = answer_form["command"] except (KeyError, StopIteration): raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD) if command not in names_map: raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD) path, iface, command = names_map[command] proxy = self.session_bus.get_object(bus_name, path) self._DBusAsyncCall(proxy, command, interface=iface) # job done, we can end the session, except if we have FLAG_LOOP if FLAG_LOOP in flags: # We have a loop, so we clear everything and we execute again the # command as we had a first call (command_elt is not used, so None # is OK) del actions[:] names_map.clear() return DBusCallback( client, None, session_data, self._c.ACTION.EXECUTE, node ) form = data_form.Form("form", title=_("Updated")) form.addField(data_form.Field("fixed", "Command sent")) status = self._c.STATUS.COMPLETED payload = None note = (self._c.NOTE.INFO, _("Command sent")) else: raise self._c.AdHocError(self._c.ERROR.INTERNAL) return (payload, status, None, note) self._c.addAdHocCommand( client, DBusCallback, adhoc_name, allowed_jids=allowed_jids, allowed_groups=allowed_groups, allowed_magics=allowed_magics, forbidden_jids=forbidden_jids, forbidden_groups=forbidden_groups, ) ## Local media ## def _adHocRemotesGet(self, profile): return self.adHocRemotesGet(self.host.getClient(profile)) @defer.inlineCallbacks def adHocRemotesGet(self, client): """Retrieve available remote media controlers in our devices @return (list[tuple[unicode, unicode, unicode]]): list of devices with: - entity full jid - device name - device label """ found_data = yield defer.ensureDeferred(self.host.findByFeatures( client, [self.host.ns_map['commands']], service=False, roster=False, own_jid=True, local_device=True)) remotes = [] for found in found_data: for device_jid_s in found: device_jid = jid.JID(device_jid_s) cmd_list = yield self._c.list(client, device_jid) for cmd in cmd_list: if cmd.nodeIdentifier == NS_MEDIA_PLAYER: try: result_elt = yield self._c.do(client, device_jid, NS_MEDIA_PLAYER, timeout=5) command_elt = self._c.getCommandElt(result_elt) form = data_form.findForm(command_elt, NS_MEDIA_PLAYER) if form is None: continue mp_options = form.fields['media_player'].options session_id = command_elt.getAttribute('sessionid') if mp_options and session_id: # we just want to discover player, so we cancel the # session self._c.do(client, device_jid, NS_MEDIA_PLAYER, action=self._c.ACTION.CANCEL, session_id=session_id) for opt in mp_options: remotes.append((device_jid_s, opt.value, opt.label or opt.value)) except Exception as e: log.warning(_( "Can't retrieve remote controllers on {device_jid}: " "{reason}".format(device_jid=device_jid, reason=e))) break defer.returnValue(remotes) def doMPRISCommand(self, proxy, command): iface, command = command.rsplit(".", 1) if command == CMD_GO_BACK: command = 'Seek' args = [-SEEK_OFFSET] elif command == CMD_GO_FWD: command = 'Seek' args = [SEEK_OFFSET] else: args = [] return self._DBusAsyncCall(proxy, command, *args, interface=iface) def addMPRISMetadata(self, form, metadata): """Serialise MRPIS Metadata according to MPRIS_METADATA_MAP""" for mpris_key, name in MPRIS_METADATA_MAP.items(): if mpris_key in metadata: value = str(metadata[mpris_key]) form.addField(data_form.Field(fieldType="fixed", var=name, value=value)) @defer.inlineCallbacks def localMediaCb(self, client, command_elt, session_data, action, node): try: x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x")) command_form = data_form.Form.fromElement(x_elt) except StopIteration: command_form = None if command_form is None or len(command_form.fields) == 0: # root request, we looks for media players bus_names = yield self._DBusListNames() bus_names = [b for b in bus_names if b.startswith(MPRIS_PREFIX)] if len(bus_names) == 0: note = (self._c.NOTE.INFO, D_("No media player found.")) defer.returnValue((None, self._c.STATUS.COMPLETED, None, note)) options = [] status = self._c.STATUS.EXECUTING form = data_form.Form("form", title=D_("Media Player Selection"), formNamespace=NS_MEDIA_PLAYER) for bus in bus_names: player_name = bus[len(MPRIS_PREFIX)+1:] if not player_name: log.warning(_("Ignoring MPRIS bus without suffix")) continue options.append(data_form.Option(bus, player_name)) field = data_form.Field( "list-single", "media_player", options=options, required=True ) form.addField(field) payload = form.toElement() defer.returnValue((payload, status, None, None)) else: # player request try: bus_name = command_form["media_player"] except KeyError: raise ValueError(_("missing media_player value")) if not bus_name.startswith(MPRIS_PREFIX): log.warning(_("Media player ad-hoc command trying to use non MPRIS bus. " "Hack attempt? Refused bus: {bus_name}").format( bus_name=bus_name)) note = (self._c.NOTE.ERROR, D_("Invalid player name.")) defer.returnValue((None, self._c.STATUS.COMPLETED, None, note)) try: proxy = self.session_bus.get_object(bus_name, MPRIS_PATH) except dbus.exceptions.DBusException as e: log.warning(_("Can't get D-Bus proxy: {reason}").format(reason=e)) note = (self._c.NOTE.ERROR, D_("Media player is not available anymore")) defer.returnValue((None, self._c.STATUS.COMPLETED, None, note)) try: command = command_form["command"] except KeyError: pass else: yield self.doMPRISCommand(proxy, command) # we construct the remote control form form = data_form.Form("form", title=D_("Media Player Selection")) form.addField(data_form.Field(fieldType="hidden", var="media_player", value=bus_name)) for iface, properties_names in MPRIS_PROPERTIES.items(): for name in properties_names: try: value = yield self._DBusGetProperty(proxy, iface, name) except Exception as e: log.warning(_("Can't retrieve attribute {name}: {reason}") .format(name=name, reason=e)) continue if name == MPRIS_METADATA_KEY: self.addMPRISMetadata(form, value) else: form.addField(data_form.Field(fieldType="fixed", var=name, value=str(value))) commands = [data_form.Option(c, c.rsplit(".", 1)[1]) for c in MPRIS_COMMANDS] form.addField(data_form.Field(fieldType="list-single", var="command", options=commands, required=True)) payload = form.toElement() status = self._c.STATUS.EXECUTING defer.returnValue((payload, status, None, None))