comparison src/plugins/plugin_xep_0050.py @ 763:ab851b46009c

plugin xep-0050 (ad-hoc commands): requesting part. first draft
author Goffi <goffi@goffi.org>
date Tue, 24 Dec 2013 15:43:52 +0100
parents 86224a13cc1d
children bfabeedbf32e
comparison
equal deleted inserted replaced
762:aed7d99276b8 763:ab851b46009c
20 from logging import debug, info, warning, error 20 from logging import debug, info, warning, error
21 from twisted.words.protocols.jabber import jid 21 from twisted.words.protocols.jabber import jid
22 from twisted.words.protocols.jabber import error as xmpp_error 22 from twisted.words.protocols.jabber import error as xmpp_error
23 from twisted.words.xish import domish 23 from twisted.words.xish import domish
24 from twisted.internet import defer, reactor 24 from twisted.internet import defer, reactor
25 from wokkel import disco, iwokkel, data_form 25 from wokkel import disco, iwokkel, data_form, compat
26 from sat.core import exceptions 26 from sat.core import exceptions
27 from sat.memory.memory import Sessions 27 from sat.memory.memory import Sessions
28 from uuid import uuid4 28 from uuid import uuid4
29 from sat.tools import xml_tools
29 30
30 from zope.interface import implements 31 from zope.interface import implements
31 32
32 try: 33 try:
33 from twisted.words.protocols.xmlstream import XMPPHandler 34 from twisted.words.protocols.xmlstream import XMPPHandler
202 ('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.6 Table 5 203 ('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.6 Table 5
203 204
204 def __init__(self, host): 205 def __init__(self, host):
205 info(_("plugin XEP-0050 initialization")) 206 info(_("plugin XEP-0050 initialization"))
206 self.host = host 207 self.host = host
207 self.requesting = {} 208 self.requesting = Sessions()
208 self.answering = {} 209 self.answering = {}
210 host.bridge.addMethod("requestCommand", ".plugin", in_sign='ss', out_sign='s',
211 method=self._requestCommandsList,
212 async=True)
213 self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True)
214 host.importMenu("Service", "commands", self._commandsMenu, help_string=_("Execute ad-hoc commands"))
209 215
210 def getHandler(self, profile): 216 def getHandler(self, profile):
211 return XEP_0050_handler(self) 217 return XEP_0050_handler(self)
212 218
213 def profileConnected(self, profile): 219 def profileConnected(self, profile):
217 try: 223 try:
218 del self.answering[profile] 224 del self.answering[profile]
219 except KeyError: 225 except KeyError:
220 pass 226 pass
221 227
228 def _items2XMLUI(self, items):
229 """ Convert discovery items to XMLUI dialog """
230 # TODO: manage items on different jids
231 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id)
232
233 form_ui.addText(_("Please select a command"), 'instructions')
234
235 options = [(item.nodeIdentifier, item.name) for item in items]
236 form_ui.addList(options, "node")
237 return form_ui
238
239 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data):
240 """
241 Convert command answer to an ui for frontend
242 @param iq_elt: command result
243 @param session_id: id of the session used with the frontend
244 @param profile_key: %(doc_profile_key)s
245
246 """
247 command_elt = iq_elt.elements(NS_COMMANDS, "command").next()
248 status = command_elt.getAttribute('status', XEP_0050.STATUS.EXECUTING)
249 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]:
250 # the command session is finished, we purge our session
251 del self.requesting[session_id]
252 return None
253 remote_session_id = command_elt.getAttribute('sessionid')
254 if remote_session_id:
255 session_data['remote_id'] = remote_session_id
256 data_elt = command_elt.elements(data_form.NS_X_DATA, 'x').next()
257 form = data_form.Form.fromElement(data_elt)
258 return xml_tools.dataForm2XMLUI(form, self.__requesting_id, session_id=session_id)
259
260 def _requestingEntity(self, data, profile):
261 """
262 request and entity and create XMLUI accordingly
263 @param data: data returned by previous XMLUI (first one must come from self._commandsMenu)
264 @param profile: %(doc_profile)s
265 @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error)
266
267 """
268 # TODO: cancel, prev and next are not managed
269 # TODO: managed answerer errors
270 # TODO: manage nodes with a non data form payload
271 if "session_id" not in data:
272 # we just had the jid, we now request it for the available commands
273 session_id, session_data = self.requesting.newSession(profile=profile)
274 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX+'jid'])
275 session_data['jid'] = entity
276 d = self.requestCommandsList(entity, profile)
277
278 def sendItems(xmlui):
279 xmlui.setSessionId(session_id) # we need to keep track of the session
280 return {'xmlui': xmlui.toXml()}
281
282 d.addCallback(sendItems)
283 else:
284 # we have started a several forms sessions
285 try:
286 session_data = self.requesting.profileGet(data["session_id"], profile)
287 except KeyError:
288 warning ("session id doesn't exist, session has probably expired")
289 # TODO: send error dialog
290 return defer.succeed({})
291 session_id = data["session_id"]
292 entity = session_data['jid']
293 try:
294 node = session_data['node']
295 # node has already been received
296 except KeyError:
297 # it's the first time we know the node, we save it in session data
298 node = session_data['node'] = data[xml_tools.SAT_FORM_PREFIX+'node']
299
300 client = self.host.getClient(profile)
301 if not client:
302 raise exceptions.ProfileUnknownError
303
304 # we request execute node's command
305 iq_elt = compat.IQ(client.xmlstream, 'set')
306 iq_elt['to'] = entity.full()
307 command_elt = iq_elt.addElement("command", NS_COMMANDS)
308 command_elt['node'] = session_data['node']
309 command_elt['action'] = XEP_0050.ACTION.EXECUTE
310 try:
311 # remote_id is the XEP_0050 sessionid used by answering command
312 # while session_id is our own session id used with the frontend
313 command_elt['sessionid'] = session_data['remote_id']
314 except KeyError:
315 pass
316
317 command_elt.addChild(xml_tools.XMLUIResultToElt(data)) # We add the XMLUI result to the command payload
318 d = iq_elt.send()
319 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data)
320 d.addCallback(lambda xmlui: {'xmlui': xmlui.toXml()} if xmlui is not None else {})
321
322 return d
323
324 def _commandsMenu(self, profile):
325 """ First XMLUI activated by menu: ask for target jid
326 @param profile: %(doc_profile)s
327
328 """
329 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id)
330 form_ui.addText(_("Please enter target jid"), 'instructions')
331 form_ui.changeLayout("pairs")
332 form_ui.addLabel("jid")
333 form_ui.addString("jid")
334 return form_ui.toXml()
335
222 def _statusCallback(self, command_elt, session_data, action, node, profile): 336 def _statusCallback(self, command_elt, session_data, action, node, profile):
223 """ Ad-hoc command used to change the "show" part of status """ 337 """ Ad-hoc command used to change the "show" part of status """
224 actions = session_data.setdefault('actions',[]) 338 actions = session_data.setdefault('actions',[])
225 actions.append(action) 339 actions.append(action)
226 340
227 if len(actions) == 1: 341 if len(actions) == 1:
342 # it's our first request, we ask the desired new status
228 status = XEP_0050.STATUS.EXECUTING 343 status = XEP_0050.STATUS.EXECUTING
229 form = data_form.Form('form', title=_('status selection')) 344 form = data_form.Form('form', title=_('status selection'))
230 show_options = [data_form.Option(name, label) for name, label in SHOWS.items()] 345 show_options = [data_form.Option(name, label) for name, label in SHOWS.items()]
231 field = data_form.Field('list-single', 'show', options=show_options, required=True) 346 field = data_form.Field('list-single', 'show', options=show_options, required=True)
232 form.addField(field) 347 form.addField(field)
233 348
234 payload = form.toElement() 349 payload = form.toElement()
235 note = None 350 note = None
236 351
237 elif len(actions) == 2: # we should have the answer here 352 elif len(actions) == 2:
353 # we should have the answer here
238 try: 354 try:
239 x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next() 355 x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next()
240 answer_form = data_form.Form.fromElement(x_elt) 356 answer_form = data_form.Form.fromElement(x_elt)
241 show = answer_form['show'] 357 show = answer_form['show']
242 except KeyError, StopIteration: 358 except KeyError, StopIteration:
256 note = (self.NOTE.INFO, _(u"Status updated")) 372 note = (self.NOTE.INFO, _(u"Status updated"))
257 else: 373 else:
258 raise AdHocError(XEP_0050.ERROR.INTERNAL) 374 raise AdHocError(XEP_0050.ERROR.INTERNAL)
259 375
260 return (payload, status, None, note) 376 return (payload, status, None, note)
377
378 def _requestCommandsList(self, to_jid_s, profile_key):
379 d = self.requestCommandsList(jid.JID(to_jid_s), profile_key)
380 d.addCallback(lambda xmlui: xmlui.toXml())
381 return d
382
383 def requestCommandsList(self, to_jid, profile_key):
384 """ Request available commands
385 @param to_jid: the entity answering the commands
386 @param profile_key: %(doc_profile)s
387
388 """
389 client = self.host.getClient(profile_key)
390 d = client.disco.requestItems(to_jid, NS_COMMANDS)
391 d.addCallback(self._items2XMLUI)
392 return d
261 393
262 def addAdHocCommand(self, callback, label, node="", features = None, timeout = 600, allowed_jids = None, allowed_groups = None, 394 def addAdHocCommand(self, callback, label, node="", features = None, timeout = 600, allowed_jids = None, allowed_groups = None,
263 allowed_magics = None, forbidden_jids = None, forbidden_groups = None, profile_key="@NONE@"): 395 allowed_magics = None, forbidden_jids = None, forbidden_groups = None, profile_key="@NONE@"):
264 """ 396 """
265 397