# HG changeset patch # User Goffi # Date 1535723220 -7200 # Node ID 8dd9db785ac81166b3e06f61ed942dbb7f9197ac # Parent bc122b68eacd2b1cf1ae9a88f95394ebeecd0693 plugin XEP-0050, adhoc D-Bus: Ad-Hoc improvment + remote media control: - "commands" namespace is now registered - added "do" and "getCommandElt" methods to XEP-0050 to run ad-hoc commands from backend - commands for a profile are now stored in client._XEP_0050_commands - Ad-Hoc D-Bus plugin can now be run without lxml or dbus (degraded) - use MPRIS to control media players - new adHocRemotesGet bridge method retrieve media players announced in all devices of the profile diff -r bc122b68eacd -r 8dd9db785ac8 sat/plugins/plugin_adhoc_dbus.py --- a/sat/plugins/plugin_adhoc_dbus.py Fri Aug 31 15:29:25 2018 +0200 +++ b/sat/plugins/plugin_adhoc_dbus.py Fri Aug 31 15:47:00 2018 +0200 @@ -17,31 +17,60 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ +from sat.core.i18n import D_, _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) -from sat.core import exceptions from twisted.internet import defer +from twisted.words.protocols.jabber import jid from wokkel import data_form try: from lxml import etree except ImportError: - raise exceptions.MissingModule( - u"Missing module lxml, please download/install it from http://lxml.de/" - ) + etree = None + log.warning(u"Missing module lxml, please download/install it from http://lxml.de/" + u"auto D-Bus discovery will be disabled") +from collections import OrderedDict import os.path import uuid -import dbus -from dbus.mainloop.glib import DBusGMainLoop +try: + import dbus + from dbus.mainloop.glib import DBusGMainLoop +except ImportError: + dbus = None + log.warning(u"Missing module dbus, please download/install it" + u"auto D-Bus discovery will be disabled") -DBusGMainLoop(set_as_default=True) +else: + DBusGMainLoop(set_as_default=True) +NS_MEDIA_PLAYER = "org.salutatoi.mediaplayer" FD_NAME = "org.freedesktop.DBus" FD_PATH = "/org/freedekstop/DBus" INTROSPECT_IFACE = "org.freedesktop.DBus.Introspectable" +MPRIS_PREFIX = u"org.mpris.MediaPlayer2" +CMD_GO_BACK = u"GoBack" +CMD_GO_FWD = u"GoFW" +SEEK_OFFSET = 5 * 1000 * 1000 +MPRIS_COMMANDS = [u"org.mpris.MediaPlayer2.Player." + cmd for cmd in ( + u"Previous", CMD_GO_BACK, u"PlayPause", CMD_GO_FWD, u"Next")] +MPRIS_PATH = u"/org/mpris/MediaPlayer2" +MPRIS_PROPERTIES = OrderedDict(( + (u"org.mpris.MediaPlayer2", ( + "Identity", + )), + (u"org.mpris.MediaPlayer2.Player", ( + "Metadata", + "PlaybackStatus", + "Volume", + )), + )) +MPRIS_METADATA_KEY = "Metadata" +MPRIS_METADATA_MAP = OrderedDict(( + ("xesam:title", u"Title"), + )) INTROSPECT_METHOD = "Introspect" IGNORED_IFACES_START = ( @@ -59,32 +88,56 @@ C.PI_DEPENDENCIES: ["XEP-0050"], C.PI_MAIN: "AdHocDBus", C.PI_HANDLER: "no", - C.PI_DESCRIPTION: _("""Add D-Bus management to Ad-Hoc commands"""), + C.PI_DESCRIPTION: _(u"""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( - "adHocDBusAddAuto", + "adHocRemotesGet", ".plugin", - in_sign="sasasasasasass", - out_sign="(sa(sss))", - method=self._adHocDBusAddAuto, + in_sign="s", + out_sign="a(sss)", + method=self._adHocRemotesGet, async=True, ) - self.session_bus = dbus.SessionBus() - self.fd_object = self.session_bus.get_object(FD_NAME, FD_PATH, introspect=False) - self.XEP_0050 = host.plugins["XEP-0050"] + self._c = host.plugins["XEP-0050"] + host.registerNamespace(u"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_(u"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: + @param kwargs: will be transmetted to the method, except for the following poped + values: - interface: name of the interface to use @return: a deferred @@ -96,6 +149,11 @@ proxy.get_dbus_method(method, dbus_interface=interface)(*args, **kwargs) return d + def _DBusGetProperty(self, proxy, interface, name): + return self._DBusAsyncCall( + proxy, u"Get", interface, name, interface=u"org.freedesktop.DBus.Properties") + + def _DBusListNames(self): return self._DBusAsyncCall(self.fd_object, "ListNames") @@ -138,40 +196,17 @@ 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, - ): + 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( - prog_name, - allowed_jids, - allowed_groups, - allowed_magics, - forbidden_jids, - forbidden_groups, - flags, - profile_key, - ) + client, prog_name, allowed_jids, allowed_groups, allowed_magics, + forbidden_jids, forbidden_groups, flags) @defer.inlineCallbacks - def adHocDBusAddAuto( - self, - prog_name, - allowed_jids=None, - allowed_groups=None, - allowed_magics=None, - forbidden_jids=None, - forbidden_groups=None, - flags=None, - profile_key=C.PROF_KEY_NONE, - ): + 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: @@ -189,6 +224,7 @@ if methods: self._addCommand( + client, prog_name, bus_name, methods, @@ -198,35 +234,24 @@ forbidden_jids=forbidden_jids, forbidden_groups=forbidden_groups, flags=flags, - profile_key=profile_key, ) defer.returnValue((bus_name, methods)) - def _addCommand( - self, - adhoc_name, - bus_name, - methods, - allowed_jids=None, - allowed_groups=None, - allowed_magics=None, - forbidden_jids=None, - forbidden_groups=None, - flags=None, - profile_key=C.PROF_KEY_NONE, - ): + 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(command_elt, session_data, action, node, profile): + 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.XEP_0050.STATUS.EXECUTING + status = self._c.STATUS.EXECUTING form = data_form.Form("form", title=_("Command selection")) options = [] for path, iface, command in methods: @@ -250,10 +275,10 @@ answer_form = data_form.Form.fromElement(x_elt) command = answer_form["command"] except (KeyError, StopIteration): - raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.BAD_PAYLOAD) + raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD) if command not in names_map: - raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.BAD_PAYLOAD) + raise self._c.AdHocError(self._c.ERROR.BAD_PAYLOAD) path, iface, command = names_map[command] proxy = self.session_bus.get_object(bus_name, path) @@ -262,23 +287,26 @@ # 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) + # 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( - None, session_data, self.XEP_0050.ACTION.EXECUTE, node, profile + client, None, session_data, self._c.ACTION.EXECUTE, node ) form = data_form.Form("form", title=_(u"Updated")) form.addField(data_form.Field("fixed", u"Command sent")) - status = self.XEP_0050.STATUS.COMPLETED + status = self._c.STATUS.COMPLETED payload = None - note = (self.XEP_0050.NOTE.INFO, _(u"Command sent")) + note = (self._c.NOTE.INFO, _(u"Command sent")) else: - raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.INTERNAL) + raise self._c.AdHocError(self._c.ERROR.INTERNAL) return (payload, status, None, note) - self.XEP_0050.addAdHocCommand( + self._c.addAdHocCommand( + client, DBusCallback, adhoc_name, allowed_jids=allowed_jids, @@ -286,5 +314,165 @@ allowed_magics=allowed_magics, forbidden_jids=forbidden_jids, forbidden_groups=forbidden_groups, - profile_key=profile_key, ) + + ## 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 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) + 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(_( + u"Can't retrieve remote controllers on {device_jid}: " + u"{reason}".format(device_jid=device_jid, reason=e))) + break + defer.returnValue(remotes) + + def doMPRISCommand(self, proxy, command): + iface, command = command.rsplit(u".", 1) + if command == CMD_GO_BACK: + command = u'Seek' + args = [-SEEK_OFFSET] + elif command == CMD_GO_FWD: + command = u'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.iteritems(): + if mpris_key in metadata: + value = unicode(metadata[mpris_key]) + form.addField(data_form.Field(fieldType=u"fixed", + var=name, + value=value)) + + @defer.inlineCallbacks + def localMediaCb(self, client, command_elt, session_data, action, node): + try: + x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next() + 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_(u"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_(u"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(_(u"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[u"media_player"] + except KeyError: + raise ValueError(_(u"missing media_player value")) + + if not bus_name.startswith(MPRIS_PREFIX): + log.warning(_(u"Media player ad-hoc command trying to use non MPRIS bus. " + u"Hack attempt? Refused bus: {bus_name}").format( + bus_name=bus_name)) + note = (self._c.NOTE.ERROR, D_(u"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(_(u"Can't get D-Bus proxy: {reason}").format(reason=e)) + note = (self._c.NOTE.ERROR, D_(u"Media player is not available anymore")) + defer.returnValue((None, self._c.STATUS.COMPLETED, None, note)) + try: + command = command_form[u"command"] + except KeyError: + pass + else: + yield self.doMPRISCommand(proxy, command) + + # we construct the remote control form + form = data_form.Form("form", title=D_(u"Media Player Selection")) + form.addField(data_form.Field(fieldType=u"hidden", + var=u"media_player", + value=bus_name)) + for iface, properties_names in MPRIS_PROPERTIES.iteritems(): + for name in properties_names: + try: + value = yield self._DBusGetProperty(proxy, iface, name) + except Exception as e: + log.warning(_(u"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=u"fixed", + var=name, + value=unicode(value))) + + commands = [data_form.Option(c, c.rsplit(u".", 1)[1]) for c in MPRIS_COMMANDS] + form.addField(data_form.Field(fieldType=u"list-single", + var=u"command", + options=commands, + required=True)) + + payload = form.toElement() + status = self._c.STATUS.EXECUTING + defer.returnValue((payload, status, None, None)) + + diff -r bc122b68eacd -r 8dd9db785ac8 sat/plugins/plugin_xep_0050.py --- a/sat/plugins/plugin_xep_0050.py Fri Aug 31 15:29:25 2018 +0200 +++ b/sat/plugins/plugin_xep_0050.py Fri Aug 31 15:47:00 2018 +0200 @@ -26,7 +26,7 @@ from twisted.words.protocols import jabber from twisted.words.xish import domish from twisted.internet import defer -from wokkel import disco, iwokkel, data_form, compat +from wokkel import disco, iwokkel, data_form from sat.core import exceptions from sat.memory.memory import Sessions from uuid import uuid4 @@ -70,7 +70,7 @@ C.PI_PROTOCOLS: ["XEP-0050"], C.PI_MAIN: "XEP_0050", C.PI_HANDLER: "yes", - C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""), + C.PI_DESCRIPTION: _(u"""Implementation of Ad-Hoc Commands"""), } @@ -86,36 +86,33 @@ class AdHocCommand(XMPPHandler): implements(iwokkel.IDisco) - def __init__( - self, - parent, - callback, - label, - node, - features, - timeout, - allowed_jids, - allowed_groups, - allowed_magics, - forbidden_jids, - forbidden_groups, - client, - ): - self.parent = parent + def __init__(self, callback, label, node, features, timeout, + allowed_jids, allowed_groups, allowed_magics, forbidden_jids, + forbidden_groups): + XMPPHandler.__init__(self) self.callback = callback self.label = label self.node = node self.features = [disco.DiscoFeature(feature) for feature in features] - self.allowed_jids, self.allowed_groups, self.allowed_magics, self.forbidden_jids, self.forbidden_groups = ( + ( + self.allowed_jids, + self.allowed_groups, + self.allowed_magics, + self.forbidden_jids, + self.forbidden_groups, + ) = ( allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, ) - self.client = client self.sessions = Sessions(timeout=timeout) + @property + def client(self): + return self.parent + def getName(self, xml_lang=None): return self.label @@ -132,12 +129,8 @@ try: allowed.update(self.client.roster.getJidsFromGroup(group)) except exceptions.UnknownGroupError: - log.warning( - _( - u"The groups [%(group)s] is unknown for profile [%(profile)s])" - % {"group": group, "profile": self.client.profile} - ) - ) + log.warning(_(u"The groups [{group}] is unknown for profile [{profile}])") + .format(group=group, profile=self.client.profile)) if requestor.userhostJID() in allowed: return True return False @@ -155,12 +148,15 @@ def _sendAnswer(self, callback_data, session_id, request): """ Send result of the command + @param callback_data: tuple (payload, status, actions, note) with: - - payload (domish.Element) usualy containing data form + - payload (domish.Element, None) usualy containing data form - status: current status, see XEP_0050.STATUS - - actions: list of allowed actions (see XEP_0050.ACTION). First action is the default one. Default to EXECUTE - - note: optional additional note: either None or a tuple with (note type, human readable string), - note type being in XEP_0050.NOTE + - actions(list[str], None): list of allowed actions (see XEP_0050.ACTION). + First action is the default one. Default to EXECUTE + - note(tuple[str, unicode]): optional additional note: either None or a + tuple with (note type, human readable string), "note type" being in + XEP_0050.NOTE @param session_id: current session id @param request: original request (domish.Element) @return: deferred @@ -200,6 +196,7 @@ def _sendError(self, error_constant, session_id, request): """ Send error stanza + @param error_constant: one of XEP_OO50.ERROR @param request: original request (domish.Element) """ @@ -235,11 +232,11 @@ else: d = defer.maybeDeferred( self.callback, + self.client, command_elt, session_data, action, self.node, - self.client.profile, ) d.addCallback(self._sendAnswer, session_id, command_elt.parent) d.addErrback( @@ -289,7 +286,6 @@ log.info(_("plugin XEP-0050 initialization")) self.host = host self.requesting = Sessions() - self.answering = {} host.bridge.addMethod( "adHocRun", ".plugin", @@ -303,7 +299,7 @@ ".plugin", in_sign="ss", out_sign="s", - method=self._list, + method=self._listUI, async=True, ) self.__requesting_id = host.registerCallback( @@ -315,23 +311,53 @@ security_limit=2, help_string=D_("Execute ad-hoc commands"), ) + host.registerNamespace(u'commands', NS_COMMANDS) def getHandler(self, client): return XEP_0050_handler(self) def profileConnected(self, client): - self.addAdHocCommand( - self._statusCallback, _("Status"), profile_key=client.profile - ) + # map from node to AdHocCommand instance + client._XEP_0050_commands = {} + self.addAdHocCommand(client, self._statusCallback, _("Status")) + + def do(self, client, entity, node, action=ACTION.EXECUTE, session_id=None, + form_values=None, timeout=30): + """Do an Ad-Hoc Command - def profileDisconnected(self, client): + @param entity(jid.JID): entity which will execture the command + @param node(unicode): node of the command + @param action(unicode): one of XEP_0050.ACTION + @param session_id(unicode, None): id of the ad-hoc session + None if no session is involved + @param form_values(dict, None): values to use to create command form + values will be passed to data_form.Form.makeFields + @return + """ + iq_elt = client.IQ(timeout=timeout) + iq_elt["to"] = entity.full() + command_elt = iq_elt.addElement("command", NS_COMMANDS) + command_elt["node"] = node + command_elt["action"] = action + if session_id is not None: + command_elt["sessionid"] = session_id + + if form_values: + # We add the XMLUI result to the command payload + form = data_form.Form("submit") + form.makeFields(form_values) + command_elt.addChild(form.toElement()) + d = iq_elt.send() + return d + + def getCommandElt(self, iq_elt): try: - del self.answering[client.profile] - except KeyError: - pass + return iq_elt.elements(NS_COMMANDS, "command").next() + except StopIteration: + raise exceptions.NotFound(_(u"Missing command element")) def _items2XMLUI(self, items, no_instructions): - """ Convert discovery items to XMLUI dialog """ + """Convert discovery items to XMLUI dialog """ # TODO: manage items on different jids form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) @@ -371,14 +397,13 @@ return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): - """ - Convert command answer to an ui for frontend + """Convert command answer to an ui for frontend + @param iq_elt: command result @param session_id: id of the session used with the frontend @param profile_key: %(doc_profile_key)s - """ - command_elt = iq_elt.elements(NS_COMMANDS, "command").next() + command_elt = self.getCommandElt(iq_elt) status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: # the command session is finished, we purge our session @@ -451,15 +476,17 @@ return d def requestingEntity(self, data, profile): - """ - request and entity and create XMLUI accordingly - @param data: data returned by previous XMLUI (first one must come from self._commandsMenu) + """Request and entity and create XMLUI accordingly. + + @param data: data returned by previous XMLUI (first one must come from + self._commandsMenu) @param profile: %(doc_profile)s - @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error) - + @return: callback dict result (with "xmlui" corresponding to the answering + dialog, or empty if it's finished without error) """ if C.bool(data.get("cancelled", C.BOOL_FALSE)): return defer.succeed({}) + data_form_values = xml_tools.XMLUIResult2DataFormResult(data) client = self.host.getClient(profile) # TODO: cancel, prev and next are not managed # TODO: managed answerer errors @@ -469,7 +496,7 @@ session_id, session_data = self.requesting.newSession(profile=client.profile) entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX + "jid"]) session_data["jid"] = entity - d = self.list(client, entity) + d = self.listUI(client, entity) def sendItems(xmlui): xmlui.session_id = session_id # we need to keep track of the session @@ -493,34 +520,24 @@ # node has already been received except KeyError: # it's the first time we know the node, we save it in session data - session_data["node"] = data[xml_tools.SAT_FORM_PREFIX + "node"] + session_data["node"] = data_form_values.pop("node") + + # remote_id is the XEP_0050 sessionid used by answering command + # while session_id is our own session id used with the frontend + remote_id = session_data.get("remote_id") # we request execute node's command - iq_elt = compat.IQ(client.xmlstream, "set") - iq_elt["to"] = entity.full() - command_elt = iq_elt.addElement("command", NS_COMMANDS) - command_elt["node"] = session_data["node"] - command_elt["action"] = XEP_0050.ACTION.EXECUTE - try: - # remote_id is the XEP_0050 sessionid used by answering command - # while session_id is our own session id used with the frontend - command_elt["sessionid"] = session_data["remote_id"] - except KeyError: - pass - - command_elt.addChild( - xml_tools.XMLUIResultToElt(data) - ) # We add the XMLUI result to the command payload - d = iq_elt.send() + d = self.do(client, entity, session_data["node"], action=XEP_0050.ACTION.EXECUTE, + session_id=remote_id, form_values=data_form_values) d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {}) return d def _commandsMenu(self, menu_data, profile): - """ First XMLUI activated by menu: ask for target jid + """First XMLUI activated by menu: ask for target jid + @param profile: %(doc_profile)s - """ form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) form_ui.addText(_("Please enter target jid"), "instructions") @@ -529,8 +546,8 @@ form_ui.addString("jid", value=self.host.getClient(profile).jid.host) return {"xmlui": form_ui.toXml()} - def _statusCallback(self, command_elt, session_data, action, node, profile): - """ Ad-hoc command used to change the "show" part of status """ + def _statusCallback(self, client, command_elt, session_data, action, node): + """Ad-hoc command used to change the "show" part of status""" actions = session_data.setdefault("actions", []) actions.append(action) @@ -560,9 +577,9 @@ if show not in SHOWS: raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) if show == "disconnect": - self.host.disconnect(profile) + self.host.disconnect(client.profile) else: - self.host.setPresence(show=show, profile_key=profile) + self.host.setPresence(show=show, profile_key=client.profile) # job done, we can end the session form = data_form.Form("form", title=_(u"Updated")) @@ -597,7 +614,7 @@ session_id, session_data = self.requesting.newSession(profile=client.profile) session_data["jid"] = service_jid if node is None: - xmlui = yield self.list(client, service_jid) + xmlui = yield self.listUI(client, service_jid) else: session_data["node"] = node cb_data = yield self.requestingEntity( @@ -608,45 +625,51 @@ xmlui.session_id = session_id defer.returnValue(xmlui) - def _list(self, to_jid_s, profile_key): + def list(self, client, to_jid): + """Request available commands + + @param to_jid(jid.JID, None): the entity answering the commands + None to use profile's server + @return D(disco.DiscoItems): found commands + """ + d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) + return d + + def _listUI(self, to_jid_s, profile_key): client = self.host.getClient(profile_key) to_jid = jid.JID(to_jid_s) if to_jid_s else None - d = self.list(client, to_jid, no_instructions=True) + d = self.listUI(client, to_jid, no_instructions=True) d.addCallback(lambda xmlui: xmlui.toXml()) return d - def list(self, client, to_jid, no_instructions=False): - """Request available commands + def listUI(self, client, to_jid, no_instructions=False): + """Request available commands and generate XMLUI @param to_jid(jid.JID, None): the entity answering the commands None to use profile's server @param no_instructions(bool): if True, don't add instructions widget + @return D(xml_tools.XMLUI): UI with the commands """ - d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) + d = self.list(client, to_jid) d.addCallback(self._items2XMLUI, no_instructions) return d - def addAdHocCommand( - self, - callback, - label, - node=None, - features=None, - timeout=600, - allowed_jids=None, - allowed_groups=None, - allowed_magics=None, - forbidden_jids=None, - forbidden_groups=None, - profile_key=C.PROF_KEY_NONE, - ): + def addAdHocCommand(self, client, callback, label, node=None, features=None, + timeout=600, allowed_jids=None, allowed_groups=None, + allowed_magics=None, forbidden_jids=None, forbidden_groups=None, + ): """Add an ad-hoc command for the current profile - @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred + @param callback: method associated with this ad-hoc command which return the + payload data (see AdHocCommand._sendAnswer), can return a + deferred @param label: label associated with this command on the main menu - @param node: disco item node associated with this command. None to use autogenerated node - @param features: features associated with the payload (list of strings), usualy data form - @param timeout: delay between two requests before canceling the session (in seconds) + @param node: disco item node associated with this command. None to use + autogenerated node + @param features: features associated with the payload (list of strings), usualy + data form + @param timeout: delay between two requests before canceling the session (in + seconds) @param allowed_jids: list of allowed entities @param allowed_groups: list of allowed roster groups @param allowed_magics: list of allowed magic keys, can be: @@ -654,7 +677,6 @@ @PROFILE_BAREJID@: allow only the jid of the profile @param forbidden_jids: black list of entities which can't access this command @param forbidden_groups: black list of groups which can't access this command - @param profile_key: profile key associated with this command, @ALL@ means can be accessed with every profiles @return: node of the added command, useful to remove the command later """ # FIXME: "@ALL@" for profile_key seems useless and dangerous @@ -676,44 +698,44 @@ if forbidden_groups is None: forbidden_groups = [] - for client in self.host.getClients(profile_key): - # TODO: manage newly created/removed profiles - _allowed_jids = ( - (allowed_jids + [client.jid.userhostJID()]) - if "@PROFILE_BAREJID@" in allowed_magics - else allowed_jids - ) - ad_hoc_command = AdHocCommand( - self, - callback, - label, - node, - features, - timeout, - _allowed_jids, - allowed_groups, - allowed_magics, - forbidden_jids, - forbidden_groups, - client, - ) - ad_hoc_command.setHandlerParent(client) - profile_commands = self.answering.setdefault(client.profile, {}) - profile_commands[node] = ad_hoc_command + # TODO: manage newly created/removed profiles + _allowed_jids = ( + (allowed_jids + [client.jid.userhostJID()]) + if "@PROFILE_BAREJID@" in allowed_magics + else allowed_jids + ) + ad_hoc_command = AdHocCommand( + callback, + label, + node, + features, + timeout, + _allowed_jids, + allowed_groups, + allowed_magics, + forbidden_jids, + forbidden_groups, + ) + ad_hoc_command.setHandlerParent(client) + commands = client._XEP_0050_commands + commands[node] = ad_hoc_command - def onCmdRequest(self, request, profile): + def onCmdRequest(self, request, client): request.handled = True requestor = jid.JID(request["from"]) command_elt = request.elements(NS_COMMANDS, "command").next() action = command_elt.getAttribute("action", self.ACTION.EXECUTE) node = command_elt.getAttribute("node") if not node: - raise exceptions.DataError + client.sendError(request, u"bad-request") + return sessionid = command_elt.getAttribute("sessionid") + commands = client._XEP_0050_commands try: - command = self.answering[profile][node] + command = commands[node] except KeyError: - raise exceptions.DataError + client.sendError(request, u"item-not-found") + return command.onRequest(command_elt, requestor, action, sessionid) @@ -723,25 +745,27 @@ def __init__(self, plugin_parent): self.plugin_parent = plugin_parent + @property + def client(self): + return self.parent + def connectionInitialized(self): self.xmlstream.addObserver( - CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile + CMD_REQUEST, self.plugin_parent.onCmdRequest, client=self.parent ) def getDiscoInfo(self, requestor, target, nodeIdentifier=""): identities = [] - if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get( - self.parent.profile - ): # we only add the identity if we have registred commands + if nodeIdentifier == NS_COMMANDS and self.client._XEP_0050_commands: + # we only add the identity if we have registred commands identities.append(ID_CMD_LIST) return [disco.DiscoFeature(NS_COMMANDS)] + identities def getDiscoItems(self, requestor, target, nodeIdentifier=""): ret = [] if nodeIdentifier == NS_COMMANDS: - for command in self.plugin_parent.answering.get( - self.parent.profile, {} - ).values(): + commands = self.client._XEP_0050_commands + for command in commands.values(): if command.isAuthorised(requestor): ret.append( disco.DiscoItem(self.parent.jid, command.node, command.getName())