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
This commit is contained in:
parent
8792a16337
commit
ae90df1a4a
2 changed files with 419 additions and 207 deletions
|
|
@ -17,31 +17,60 @@
|
|||
# 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 _
|
||||
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 @@ PLUGIN_INFO = {
|
|||
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 @@ class AdHocDBus(object):
|
|||
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 @@ class AdHocDBus(object):
|
|||
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 @@ class AdHocDBus(object):
|
|||
|
||||
if methods:
|
||||
self._addCommand(
|
||||
client,
|
||||
prog_name,
|
||||
bus_name,
|
||||
methods,
|
||||
|
|
@ -198,35 +234,24 @@ class AdHocDBus(object):
|
|||
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 @@ class AdHocDBus(object):
|
|||
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 @@ class AdHocDBus(object):
|
|||
|
||||
# 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 @@ class AdHocDBus(object):
|
|||
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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from twisted.words.protocols.jabber import jid
|
|||
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 @@ PLUGIN_INFO = {
|
|||
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 AdHocError(Exception):
|
|||
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 @@ class AdHocCommand(XMPPHandler):
|
|||
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 @@ class AdHocCommand(XMPPHandler):
|
|||
|
||||
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 @@ class AdHocCommand(XMPPHandler):
|
|||
|
||||
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 @@ class AdHocCommand(XMPPHandler):
|
|||
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 @@ class XEP_0050(object):
|
|||
log.info(_("plugin XEP-0050 initialization"))
|
||||
self.host = host
|
||||
self.requesting = Sessions()
|
||||
self.answering = {}
|
||||
host.bridge.addMethod(
|
||||
"adHocRun",
|
||||
".plugin",
|
||||
|
|
@ -303,7 +299,7 @@ class XEP_0050(object):
|
|||
".plugin",
|
||||
in_sign="ss",
|
||||
out_sign="s",
|
||||
method=self._list,
|
||||
method=self._listUI,
|
||||
async=True,
|
||||
)
|
||||
self.__requesting_id = host.registerCallback(
|
||||
|
|
@ -315,23 +311,53 @@ class XEP_0050(object):
|
|||
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 profileDisconnected(self, client):
|
||||
def do(self, client, entity, node, action=ACTION.EXECUTE, session_id=None,
|
||||
form_values=None, timeout=30):
|
||||
"""Do an Ad-Hoc Command
|
||||
|
||||
@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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050(object):
|
|||
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)
|
||||
@param profile: %(doc_profile)s
|
||||
@return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error)
|
||||
"""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)
|
||||
"""
|
||||
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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050(object):
|
|||
# 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
|
||||
@param profile: %(doc_profile)s
|
||||
"""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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050(object):
|
|||
xmlui.session_id = session_id
|
||||
defer.returnValue(xmlui)
|
||||
|
||||
def _list(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.addCallback(lambda xmlui: xmlui.toXml())
|
||||
return d
|
||||
|
||||
def list(self, client, to_jid, no_instructions=False):
|
||||
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
|
||||
@param no_instructions(bool): if True, don't add instructions widget
|
||||
@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.listUI(client, to_jid, no_instructions=True)
|
||||
d.addCallback(lambda xmlui: xmlui.toXml())
|
||||
return d
|
||||
|
||||
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.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 @@ class XEP_0050(object):
|
|||
@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 @@ class XEP_0050(object):
|
|||
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 @@ class XEP_0050_handler(XMPPHandler):
|
|||
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())
|
||||
|
|
|
|||
Loading…
Reference in a new issue