comparison sat/plugins/plugin_xep_0050.py @ 2624:56f94936df1e

code style reformatting using black
author Goffi <goffi@goffi.org>
date Wed, 27 Jun 2018 20:14:46 +0200
parents 0112c1f7dcf0
children 8dd9db785ac8
comparison
equal deleted inserted replaced
2623:49533de4540b 2624:56f94936df1e
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _, D_ 20 from sat.core.i18n import _, D_
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23
23 log = getLogger(__name__) 24 log = getLogger(__name__)
24 from twisted.words.protocols.jabber import jid 25 from twisted.words.protocols.jabber import jid
25 from twisted.words.protocols import jabber 26 from twisted.words.protocols import jabber
26 from twisted.words.xish import domish 27 from twisted.words.xish import domish
27 from twisted.internet import defer 28 from twisted.internet import defer
39 from wokkel.subprotocols import XMPPHandler 40 from wokkel.subprotocols import XMPPHandler
40 41
41 from collections import namedtuple 42 from collections import namedtuple
42 43
43 try: 44 try:
44 from collections import OrderedDict # only available from python 2.7 45 from collections import OrderedDict # only available from python 2.7
45 except ImportError: 46 except ImportError:
46 from ordereddict import OrderedDict 47 from ordereddict import OrderedDict
47 48
48 IQ_SET = '/iq[@type="set"]' 49 IQ_SET = '/iq[@type="set"]'
49 NS_COMMANDS = "http://jabber.org/protocol/commands" 50 NS_COMMANDS = "http://jabber.org/protocol/commands"
50 ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list") 51 ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list")
51 ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") 52 ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node")
52 CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' 53 CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]'
53 54
54 SHOWS = OrderedDict([('default', _('Online')), 55 SHOWS = OrderedDict(
55 ('away', _('Away')), 56 [
56 ('chat', _('Free for chat')), 57 ("default", _("Online")),
57 ('dnd', _('Do not disturb')), 58 ("away", _("Away")),
58 ('xa', _('Left')), 59 ("chat", _("Free for chat")),
59 ('disconnect', _('Disconnect'))]) 60 ("dnd", _("Do not disturb")),
61 ("xa", _("Left")),
62 ("disconnect", _("Disconnect")),
63 ]
64 )
60 65
61 PLUGIN_INFO = { 66 PLUGIN_INFO = {
62 C.PI_NAME: "Ad-Hoc Commands", 67 C.PI_NAME: "Ad-Hoc Commands",
63 C.PI_IMPORT_NAME: "XEP-0050", 68 C.PI_IMPORT_NAME: "XEP-0050",
64 C.PI_TYPE: "XEP", 69 C.PI_TYPE: "XEP",
65 C.PI_PROTOCOLS: ["XEP-0050"], 70 C.PI_PROTOCOLS: ["XEP-0050"],
66 C.PI_MAIN: "XEP_0050", 71 C.PI_MAIN: "XEP_0050",
67 C.PI_HANDLER: "yes", 72 C.PI_HANDLER: "yes",
68 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands""") 73 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""),
69 } 74 }
70 75
71 76
72 class AdHocError(Exception): 77 class AdHocError(Exception):
73
74 def __init__(self, error_const): 78 def __init__(self, error_const):
75 """ Error to be used from callback 79 """ Error to be used from callback
76 @param error_const: one of XEP_0050.ERROR 80 @param error_const: one of XEP_0050.ERROR
77 """ 81 """
78 assert error_const in XEP_0050.ERROR 82 assert error_const in XEP_0050.ERROR
79 self.callback_error = error_const 83 self.callback_error = error_const
80 84
85
81 class AdHocCommand(XMPPHandler): 86 class AdHocCommand(XMPPHandler):
82 implements(iwokkel.IDisco) 87 implements(iwokkel.IDisco)
83 88
84 def __init__(self, parent, callback, label, node, features, timeout, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client): 89 def __init__(
90 self,
91 parent,
92 callback,
93 label,
94 node,
95 features,
96 timeout,
97 allowed_jids,
98 allowed_groups,
99 allowed_magics,
100 forbidden_jids,
101 forbidden_groups,
102 client,
103 ):
85 self.parent = parent 104 self.parent = parent
86 self.callback = callback 105 self.callback = callback
87 self.label = label 106 self.label = label
88 self.node = node 107 self.node = node
89 self.features = [disco.DiscoFeature(feature) for feature in features] 108 self.features = [disco.DiscoFeature(feature) for feature in features]
90 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 109 self.allowed_jids, self.allowed_groups, self.allowed_magics, self.forbidden_jids, self.forbidden_groups = (
110 allowed_jids,
111 allowed_groups,
112 allowed_magics,
113 forbidden_jids,
114 forbidden_groups,
115 )
91 self.client = client 116 self.client = client
92 self.sessions = Sessions(timeout=timeout) 117 self.sessions = Sessions(timeout=timeout)
93 118
94 def getName(self, xml_lang=None): 119 def getName(self, xml_lang=None):
95 return self.label 120 return self.label
96 121
97 def isAuthorised(self, requestor): 122 def isAuthorised(self, requestor):
98 if '@ALL@' in self.allowed_magics: 123 if "@ALL@" in self.allowed_magics:
99 return True 124 return True
100 forbidden = set(self.forbidden_jids) 125 forbidden = set(self.forbidden_jids)
101 for group in self.forbidden_groups: 126 for group in self.forbidden_groups:
102 forbidden.update(self.client.roster.getJidsFromGroup(group)) 127 forbidden.update(self.client.roster.getJidsFromGroup(group))
103 if requestor.userhostJID() in forbidden: 128 if requestor.userhostJID() in forbidden:
105 allowed = set(self.allowed_jids) 130 allowed = set(self.allowed_jids)
106 for group in self.allowed_groups: 131 for group in self.allowed_groups:
107 try: 132 try:
108 allowed.update(self.client.roster.getJidsFromGroup(group)) 133 allowed.update(self.client.roster.getJidsFromGroup(group))
109 except exceptions.UnknownGroupError: 134 except exceptions.UnknownGroupError:
110 log.warning(_(u"The groups [%(group)s] is unknown for profile [%(profile)s])" % {'group':group, 'profile':self.client.profile})) 135 log.warning(
136 _(
137 u"The groups [%(group)s] is unknown for profile [%(profile)s])"
138 % {"group": group, "profile": self.client.profile}
139 )
140 )
111 if requestor.userhostJID() in allowed: 141 if requestor.userhostJID() in allowed:
112 return True 142 return True
113 return False 143 return False
114 144
115 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 145 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
116 if nodeIdentifier != NS_COMMANDS: # FIXME: we should manage other disco nodes here 146 if (
147 nodeIdentifier != NS_COMMANDS
148 ): # FIXME: we should manage other disco nodes here
117 return [] 149 return []
118 # identities = [ID_CMD_LIST if self.node == NS_COMMANDS else ID_CMD_NODE] # FIXME 150 # identities = [ID_CMD_LIST if self.node == NS_COMMANDS else ID_CMD_NODE] # FIXME
119 return [disco.DiscoFeature(NS_COMMANDS)] + self.features 151 return [disco.DiscoFeature(NS_COMMANDS)] + self.features
120 152
121 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 153 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
122 return [] 154 return []
123 155
124 def _sendAnswer(self, callback_data, session_id, request): 156 def _sendAnswer(self, callback_data, session_id, request):
125 """ Send result of the command 157 """ Send result of the command
126 @param callback_data: tuple (payload, status, actions, note) with: 158 @param callback_data: tuple (payload, status, actions, note) with:
132 @param session_id: current session id 164 @param session_id: current session id
133 @param request: original request (domish.Element) 165 @param request: original request (domish.Element)
134 @return: deferred 166 @return: deferred
135 """ 167 """
136 payload, status, actions, note = callback_data 168 payload, status, actions, note = callback_data
137 assert(isinstance(payload, domish.Element) or payload is None) 169 assert isinstance(payload, domish.Element) or payload is None
138 assert(status in XEP_0050.STATUS) 170 assert status in XEP_0050.STATUS
139 if not actions: 171 if not actions:
140 actions = [XEP_0050.ACTION.EXECUTE] 172 actions = [XEP_0050.ACTION.EXECUTE]
141 result = domish.Element((None, 'iq')) 173 result = domish.Element((None, "iq"))
142 result['type'] = 'result' 174 result["type"] = "result"
143 result['id'] = request['id'] 175 result["id"] = request["id"]
144 result['to'] = request['from'] 176 result["to"] = request["from"]
145 command_elt = result.addElement('command', NS_COMMANDS) 177 command_elt = result.addElement("command", NS_COMMANDS)
146 command_elt['sessionid'] = session_id 178 command_elt["sessionid"] = session_id
147 command_elt['node'] = self.node 179 command_elt["node"] = self.node
148 command_elt['status'] = status 180 command_elt["status"] = status
149 181
150 if status != XEP_0050.STATUS.CANCELED: 182 if status != XEP_0050.STATUS.CANCELED:
151 if status != XEP_0050.STATUS.COMPLETED: 183 if status != XEP_0050.STATUS.COMPLETED:
152 actions_elt = command_elt.addElement('actions') 184 actions_elt = command_elt.addElement("actions")
153 actions_elt['execute'] = actions[0] 185 actions_elt["execute"] = actions[0]
154 for action in actions: 186 for action in actions:
155 actions_elt.addElement(action) 187 actions_elt.addElement(action)
156 188
157 if note is not None: 189 if note is not None:
158 note_type, note_mess = note 190 note_type, note_mess = note
159 note_elt = command_elt.addElement('note', content=note_mess) 191 note_elt = command_elt.addElement("note", content=note_mess)
160 note_elt['type'] = note_type 192 note_elt["type"] = note_type
161 193
162 if payload is not None: 194 if payload is not None:
163 command_elt.addChild(payload) 195 command_elt.addChild(payload)
164 196
165 self.client.send(result) 197 self.client.send(result)
179 self.client.send(iq_elt) 211 self.client.send(iq_elt)
180 del self.sessions[session_id] 212 del self.sessions[session_id]
181 213
182 def onRequest(self, command_elt, requestor, action, session_id): 214 def onRequest(self, command_elt, requestor, action, session_id):
183 if not self.isAuthorised(requestor): 215 if not self.isAuthorised(requestor):
184 return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) 216 return self._sendError(
217 XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent
218 )
185 if session_id: 219 if session_id:
186 try: 220 try:
187 session_data = self.sessions[session_id] 221 session_data = self.sessions[session_id]
188 except KeyError: 222 except KeyError:
189 return self._sendError(XEP_0050.ERROR.SESSION_EXPIRED, session_id, command_elt.parent) 223 return self._sendError(
190 if session_data['requestor'] != requestor: 224 XEP_0050.ERROR.SESSION_EXPIRED, session_id, command_elt.parent
191 return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) 225 )
226 if session_data["requestor"] != requestor:
227 return self._sendError(
228 XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent
229 )
192 else: 230 else:
193 session_id, session_data = self.sessions.newSession() 231 session_id, session_data = self.sessions.newSession()
194 session_data['requestor'] = requestor 232 session_data["requestor"] = requestor
195 if action == XEP_0050.ACTION.CANCEL: 233 if action == XEP_0050.ACTION.CANCEL:
196 d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None)) 234 d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None))
197 else: 235 else:
198 d = defer.maybeDeferred(self.callback, command_elt, session_data, action, self.node, self.client.profile) 236 d = defer.maybeDeferred(
237 self.callback,
238 command_elt,
239 session_data,
240 action,
241 self.node,
242 self.client.profile,
243 )
199 d.addCallback(self._sendAnswer, session_id, command_elt.parent) 244 d.addCallback(self._sendAnswer, session_id, command_elt.parent)
200 d.addErrback(lambda failure, request: self._sendError(failure.value.callback_error, session_id, request), command_elt.parent) 245 d.addErrback(
246 lambda failure, request: self._sendError(
247 failure.value.callback_error, session_id, request
248 ),
249 command_elt.parent,
250 )
201 251
202 252
203 class XEP_0050(object): 253 class XEP_0050(object):
204 STATUS = namedtuple('Status', ('EXECUTING', 'COMPLETED', 'CANCELED'))('executing', 'completed', 'canceled') 254 STATUS = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))(
205 ACTION = namedtuple('Action', ('EXECUTE', 'CANCEL', 'NEXT', 'PREV'))('execute', 'cancel', 'next', 'prev') 255 "executing", "completed", "canceled"
206 NOTE = namedtuple('Note', ('INFO','WARN','ERROR'))('info','warn','error') 256 )
207 ERROR = namedtuple('Error', ('MALFORMED_ACTION', 'BAD_ACTION', 'BAD_LOCALE', 'BAD_PAYLOAD', 'BAD_SESSIONID', 'SESSION_EXPIRED', 257 ACTION = namedtuple("Action", ("EXECUTE", "CANCEL", "NEXT", "PREV"))(
208 'FORBIDDEN', 'ITEM_NOT_FOUND', 'FEATURE_NOT_IMPLEMENTED', 'INTERNAL'))(('bad-request', 'malformed-action'), 258 "execute", "cancel", "next", "prev"
209 ('bad-request', 'bad-action'), ('bad-request', 'bad-locale'), ('bad-request','bad-payload'), 259 )
210 ('bad-request','bad-sessionid'), ('not-allowed','session-expired'), ('forbidden', None), 260 NOTE = namedtuple("Note", ("INFO", "WARN", "ERROR"))("info", "warn", "error")
211 ('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.4 Table 5 261 ERROR = namedtuple(
262 "Error",
263 (
264 "MALFORMED_ACTION",
265 "BAD_ACTION",
266 "BAD_LOCALE",
267 "BAD_PAYLOAD",
268 "BAD_SESSIONID",
269 "SESSION_EXPIRED",
270 "FORBIDDEN",
271 "ITEM_NOT_FOUND",
272 "FEATURE_NOT_IMPLEMENTED",
273 "INTERNAL",
274 ),
275 )(
276 ("bad-request", "malformed-action"),
277 ("bad-request", "bad-action"),
278 ("bad-request", "bad-locale"),
279 ("bad-request", "bad-payload"),
280 ("bad-request", "bad-sessionid"),
281 ("not-allowed", "session-expired"),
282 ("forbidden", None),
283 ("item-not-found", None),
284 ("feature-not-implemented", None),
285 ("internal-server-error", None),
286 ) # XEP-0050 §4.4 Table 5
212 287
213 def __init__(self, host): 288 def __init__(self, host):
214 log.info(_("plugin XEP-0050 initialization")) 289 log.info(_("plugin XEP-0050 initialization"))
215 self.host = host 290 self.host = host
216 self.requesting = Sessions() 291 self.requesting = Sessions()
217 self.answering = {} 292 self.answering = {}
218 host.bridge.addMethod("adHocRun", ".plugin", in_sign='sss', out_sign='s', 293 host.bridge.addMethod(
219 method=self._run, 294 "adHocRun",
220 async=True) 295 ".plugin",
221 host.bridge.addMethod("adHocList", ".plugin", in_sign='ss', out_sign='s', 296 in_sign="sss",
222 method=self._list, 297 out_sign="s",
223 async=True) 298 method=self._run,
224 self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True) 299 async=True,
225 host.importMenu((D_("Service"), D_("Commands")), self._commandsMenu, security_limit=2, help_string=D_("Execute ad-hoc commands")) 300 )
301 host.bridge.addMethod(
302 "adHocList",
303 ".plugin",
304 in_sign="ss",
305 out_sign="s",
306 method=self._list,
307 async=True,
308 )
309 self.__requesting_id = host.registerCallback(
310 self._requestingEntity, with_data=True
311 )
312 host.importMenu(
313 (D_("Service"), D_("Commands")),
314 self._commandsMenu,
315 security_limit=2,
316 help_string=D_("Execute ad-hoc commands"),
317 )
226 318
227 def getHandler(self, client): 319 def getHandler(self, client):
228 return XEP_0050_handler(self) 320 return XEP_0050_handler(self)
229 321
230 def profileConnected(self, client): 322 def profileConnected(self, client):
231 self.addAdHocCommand(self._statusCallback, _("Status"), profile_key=client.profile) 323 self.addAdHocCommand(
324 self._statusCallback, _("Status"), profile_key=client.profile
325 )
232 326
233 def profileDisconnected(self, client): 327 def profileDisconnected(self, client):
234 try: 328 try:
235 del self.answering[client.profile] 329 del self.answering[client.profile]
236 except KeyError: 330 except KeyError:
240 """ Convert discovery items to XMLUI dialog """ 334 """ Convert discovery items to XMLUI dialog """
241 # TODO: manage items on different jids 335 # TODO: manage items on different jids
242 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) 336 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id)
243 337
244 if not no_instructions: 338 if not no_instructions:
245 form_ui.addText(_("Please select a command"), 'instructions') 339 form_ui.addText(_("Please select a command"), "instructions")
246 340
247 options = [(item.nodeIdentifier, item.name) for item in items] 341 options = [(item.nodeIdentifier, item.name) for item in items]
248 form_ui.addList("node", options) 342 form_ui.addList("node", options)
249 return form_ui 343 return form_ui
250 344
252 """Return the constant corresponding to <note/> type attribute value 346 """Return the constant corresponding to <note/> type attribute value
253 347
254 @param type_: note type (see XEP-0050 §4.3) 348 @param type_: note type (see XEP-0050 §4.3)
255 @return: a C.XMLUI_DATA_LVL_* constant 349 @return: a C.XMLUI_DATA_LVL_* constant
256 """ 350 """
257 if type_ == 'error': 351 if type_ == "error":
258 return C.XMLUI_DATA_LVL_ERROR 352 return C.XMLUI_DATA_LVL_ERROR
259 elif type_ == 'warn': 353 elif type_ == "warn":
260 return C.XMLUI_DATA_LVL_WARNING 354 return C.XMLUI_DATA_LVL_WARNING
261 else: 355 else:
262 if type_ != 'info': 356 if type_ != "info":
263 log.warning(_(u"Invalid note type [%s], using info") % type_) 357 log.warning(_(u"Invalid note type [%s], using info") % type_)
264 return C.XMLUI_DATA_LVL_INFO 358 return C.XMLUI_DATA_LVL_INFO
265 359
266 def _mergeNotes(self, notes): 360 def _mergeNotes(self, notes):
267 """Merge notes with level prefix (e.g. "ERROR: the message") 361 """Merge notes with level prefix (e.g. "ERROR: the message")
268 362
269 @param notes (list): list of tuple (level, message) 363 @param notes (list): list of tuple (level, message)
270 @return: list of messages 364 @return: list of messages
271 """ 365 """
272 lvl_map = {C.XMLUI_DATA_LVL_INFO: '', 366 lvl_map = {
273 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), 367 C.XMLUI_DATA_LVL_INFO: "",
274 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR") 368 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"),
275 } 369 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"),
370 }
276 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] 371 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes]
277 372
278 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): 373 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data):
279 """ 374 """
280 Convert command answer to an ui for frontend 375 Convert command answer to an ui for frontend
282 @param session_id: id of the session used with the frontend 377 @param session_id: id of the session used with the frontend
283 @param profile_key: %(doc_profile_key)s 378 @param profile_key: %(doc_profile_key)s
284 379
285 """ 380 """
286 command_elt = iq_elt.elements(NS_COMMANDS, "command").next() 381 command_elt = iq_elt.elements(NS_COMMANDS, "command").next()
287 status = command_elt.getAttribute('status', XEP_0050.STATUS.EXECUTING) 382 status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING)
288 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: 383 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]:
289 # the command session is finished, we purge our session 384 # the command session is finished, we purge our session
290 del self.requesting[session_id] 385 del self.requesting[session_id]
291 if status == XEP_0050.STATUS.COMPLETED: 386 if status == XEP_0050.STATUS.COMPLETED:
292 session_id = None 387 session_id = None
293 else: 388 else:
294 return None 389 return None
295 remote_session_id = command_elt.getAttribute('sessionid') 390 remote_session_id = command_elt.getAttribute("sessionid")
296 if remote_session_id: 391 if remote_session_id:
297 session_data['remote_id'] = remote_session_id 392 session_data["remote_id"] = remote_session_id
298 notes = [] 393 notes = []
299 for note_elt in command_elt.elements(NS_COMMANDS, 'note'): 394 for note_elt in command_elt.elements(NS_COMMANDS, "note"):
300 notes.append((self._getDataLvl(note_elt.getAttribute('type', 'info')), 395 notes.append(
301 unicode(note_elt))) 396 (
302 for data_elt in command_elt.elements(data_form.NS_X_DATA, 'x'): 397 self._getDataLvl(note_elt.getAttribute("type", "info")),
303 if data_elt['type'] in ('form', 'result'): 398 unicode(note_elt),
399 )
400 )
401 for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"):
402 if data_elt["type"] in ("form", "result"):
304 break 403 break
305 else: 404 else:
306 # no matching data element found 405 # no matching data element found
307 if status != XEP_0050.STATUS.COMPLETED: 406 if status != XEP_0050.STATUS.COMPLETED:
308 log.warning(_("No known payload found in ad-hoc command result, aborting")) 407 log.warning(
408 _("No known payload found in ad-hoc command result, aborting")
409 )
309 del self.requesting[session_id] 410 del self.requesting[session_id]
310 return xml_tools.XMLUI(C.XMLUI_DIALOG, 411 return xml_tools.XMLUI(
311 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, 412 C.XMLUI_DIALOG,
312 C.XMLUI_DATA_MESS: _("No payload found"), 413 dialog_opt={
313 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_ERROR, 414 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
314 } 415 C.XMLUI_DATA_MESS: _("No payload found"),
315 ) 416 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_ERROR,
417 },
418 )
316 if not notes: 419 if not notes:
317 # the status is completed, and we have no note to show 420 # the status is completed, and we have no note to show
318 return None 421 return None
319 422
320 # if we have only one note, we show a dialog with the level of the note 423 # if we have only one note, we show a dialog with the level of the note
321 # if we have more, we show a dialog with "info" level, and all notes merged 424 # if we have more, we show a dialog with "info" level, and all notes merged
322 dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO 425 dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO
323 return xml_tools.XMLUI( 426 return xml_tools.XMLUI(
324 C.XMLUI_DIALOG, 427 C.XMLUI_DIALOG,
325 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, 428 dialog_opt={
326 C.XMLUI_DATA_MESS: u'\n'.join(self._mergeNotes(notes)), 429 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
327 C.XMLUI_DATA_LVL: dlg_level, 430 C.XMLUI_DATA_MESS: u"\n".join(self._mergeNotes(notes)),
328 }, 431 C.XMLUI_DATA_LVL: dlg_level,
329 session_id = session_id 432 },
330 ) 433 session_id=session_id,
434 )
331 435
332 if session_id is None: 436 if session_id is None:
333 return xml_tools.dataFormEltResult2XMLUI(data_elt) 437 return xml_tools.dataFormEltResult2XMLUI(data_elt)
334 form = data_form.Form.fromElement(data_elt) 438 form = data_form.Form.fromElement(data_elt)
335 # we add any present note to the instructions 439 # we add any present note to the instructions
336 form.instructions.extend(self._mergeNotes(notes)) 440 form.instructions.extend(self._mergeNotes(notes))
337 return xml_tools.dataForm2XMLUI(form, self.__requesting_id, session_id=session_id) 441 return xml_tools.dataForm2XMLUI(form, self.__requesting_id, session_id=session_id)
338 442
339 def _requestingEntity(self, data, profile): 443 def _requestingEntity(self, data, profile):
340 def serialise(ret_data): 444 def serialise(ret_data):
341 if 'xmlui' in ret_data: 445 if "xmlui" in ret_data:
342 ret_data['xmlui'] = ret_data['xmlui'].toXml() 446 ret_data["xmlui"] = ret_data["xmlui"].toXml()
343 return ret_data 447 return ret_data
344 448
345 d = self.requestingEntity(data, profile) 449 d = self.requestingEntity(data, profile)
346 d.addCallback(serialise) 450 d.addCallback(serialise)
347 return d 451 return d
352 @param data: data returned by previous XMLUI (first one must come from self._commandsMenu) 456 @param data: data returned by previous XMLUI (first one must come from self._commandsMenu)
353 @param profile: %(doc_profile)s 457 @param profile: %(doc_profile)s
354 @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error) 458 @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error)
355 459
356 """ 460 """
357 if C.bool(data.get('cancelled', C.BOOL_FALSE)): 461 if C.bool(data.get("cancelled", C.BOOL_FALSE)):
358 return defer.succeed({}) 462 return defer.succeed({})
359 client = self.host.getClient(profile) 463 client = self.host.getClient(profile)
360 # TODO: cancel, prev and next are not managed 464 # TODO: cancel, prev and next are not managed
361 # TODO: managed answerer errors 465 # TODO: managed answerer errors
362 # TODO: manage nodes with a non data form payload 466 # TODO: manage nodes with a non data form payload
363 if "session_id" not in data: 467 if "session_id" not in data:
364 # we just had the jid, we now request it for the available commands 468 # we just had the jid, we now request it for the available commands
365 session_id, session_data = self.requesting.newSession(profile=client.profile) 469 session_id, session_data = self.requesting.newSession(profile=client.profile)
366 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX+'jid']) 470 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX + "jid"])
367 session_data['jid'] = entity 471 session_data["jid"] = entity
368 d = self.list(client, entity) 472 d = self.list(client, entity)
369 473
370 def sendItems(xmlui): 474 def sendItems(xmlui):
371 xmlui.session_id = session_id # we need to keep track of the session 475 xmlui.session_id = session_id # we need to keep track of the session
372 return {'xmlui': xmlui} 476 return {"xmlui": xmlui}
373 477
374 d.addCallback(sendItems) 478 d.addCallback(sendItems)
375 else: 479 else:
376 # we have started a several forms sessions 480 # we have started a several forms sessions
377 try: 481 try:
378 session_data = self.requesting.profileGet(data["session_id"], client.profile) 482 session_data = self.requesting.profileGet(
483 data["session_id"], client.profile
484 )
379 except KeyError: 485 except KeyError:
380 log.warning ("session id doesn't exist, session has probably expired") 486 log.warning("session id doesn't exist, session has probably expired")
381 # TODO: send error dialog 487 # TODO: send error dialog
382 return defer.succeed({}) 488 return defer.succeed({})
383 session_id = data["session_id"] 489 session_id = data["session_id"]
384 entity = session_data['jid'] 490 entity = session_data["jid"]
385 try: 491 try:
386 session_data['node'] 492 session_data["node"]
387 # node has already been received 493 # node has already been received
388 except KeyError: 494 except KeyError:
389 # it's the first time we know the node, we save it in session data 495 # it's the first time we know the node, we save it in session data
390 session_data['node'] = data[xml_tools.SAT_FORM_PREFIX+'node'] 496 session_data["node"] = data[xml_tools.SAT_FORM_PREFIX + "node"]
391 497
392 # we request execute node's command 498 # we request execute node's command
393 iq_elt = compat.IQ(client.xmlstream, 'set') 499 iq_elt = compat.IQ(client.xmlstream, "set")
394 iq_elt['to'] = entity.full() 500 iq_elt["to"] = entity.full()
395 command_elt = iq_elt.addElement("command", NS_COMMANDS) 501 command_elt = iq_elt.addElement("command", NS_COMMANDS)
396 command_elt['node'] = session_data['node'] 502 command_elt["node"] = session_data["node"]
397 command_elt['action'] = XEP_0050.ACTION.EXECUTE 503 command_elt["action"] = XEP_0050.ACTION.EXECUTE
398 try: 504 try:
399 # remote_id is the XEP_0050 sessionid used by answering command 505 # remote_id is the XEP_0050 sessionid used by answering command
400 # while session_id is our own session id used with the frontend 506 # while session_id is our own session id used with the frontend
401 command_elt['sessionid'] = session_data['remote_id'] 507 command_elt["sessionid"] = session_data["remote_id"]
402 except KeyError: 508 except KeyError:
403 pass 509 pass
404 510
405 command_elt.addChild(xml_tools.XMLUIResultToElt(data)) # We add the XMLUI result to the command payload 511 command_elt.addChild(
512 xml_tools.XMLUIResultToElt(data)
513 ) # We add the XMLUI result to the command payload
406 d = iq_elt.send() 514 d = iq_elt.send()
407 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) 515 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data)
408 d.addCallback(lambda xmlui: {'xmlui': xmlui} if xmlui is not None else {}) 516 d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {})
409 517
410 return d 518 return d
411 519
412 def _commandsMenu(self, menu_data, profile): 520 def _commandsMenu(self, menu_data, profile):
413 """ First XMLUI activated by menu: ask for target jid 521 """ First XMLUI activated by menu: ask for target jid
414 @param profile: %(doc_profile)s 522 @param profile: %(doc_profile)s
415 523
416 """ 524 """
417 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) 525 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id)
418 form_ui.addText(_("Please enter target jid"), 'instructions') 526 form_ui.addText(_("Please enter target jid"), "instructions")
419 form_ui.changeContainer("pairs") 527 form_ui.changeContainer("pairs")
420 form_ui.addLabel("jid") 528 form_ui.addLabel("jid")
421 form_ui.addString("jid", value=self.host.getClient(profile).jid.host) 529 form_ui.addString("jid", value=self.host.getClient(profile).jid.host)
422 return {'xmlui': form_ui.toXml()} 530 return {"xmlui": form_ui.toXml()}
423 531
424 def _statusCallback(self, command_elt, session_data, action, node, profile): 532 def _statusCallback(self, command_elt, session_data, action, node, profile):
425 """ Ad-hoc command used to change the "show" part of status """ 533 """ Ad-hoc command used to change the "show" part of status """
426 actions = session_data.setdefault('actions',[]) 534 actions = session_data.setdefault("actions", [])
427 actions.append(action) 535 actions.append(action)
428 536
429 if len(actions) == 1: 537 if len(actions) == 1:
430 # it's our first request, we ask the desired new status 538 # it's our first request, we ask the desired new status
431 status = XEP_0050.STATUS.EXECUTING 539 status = XEP_0050.STATUS.EXECUTING
432 form = data_form.Form('form', title=_('status selection')) 540 form = data_form.Form("form", title=_("status selection"))
433 show_options = [data_form.Option(name, label) for name, label in SHOWS.items()] 541 show_options = [
434 field = data_form.Field('list-single', 'show', options=show_options, required=True) 542 data_form.Option(name, label) for name, label in SHOWS.items()
543 ]
544 field = data_form.Field(
545 "list-single", "show", options=show_options, required=True
546 )
435 form.addField(field) 547 form.addField(field)
436 548
437 payload = form.toElement() 549 payload = form.toElement()
438 note = None 550 note = None
439 551
440 elif len(actions) == 2: 552 elif len(actions) == 2:
441 # we should have the answer here 553 # we should have the answer here
442 try: 554 try:
443 x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next() 555 x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next()
444 answer_form = data_form.Form.fromElement(x_elt) 556 answer_form = data_form.Form.fromElement(x_elt)
445 show = answer_form['show'] 557 show = answer_form["show"]
446 except (KeyError, StopIteration): 558 except (KeyError, StopIteration):
447 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) 559 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD)
448 if show not in SHOWS: 560 if show not in SHOWS:
449 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) 561 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD)
450 if show == "disconnect": 562 if show == "disconnect":
451 self.host.disconnect(profile) 563 self.host.disconnect(profile)
452 else: 564 else:
453 self.host.setPresence(show=show, profile_key=profile) 565 self.host.setPresence(show=show, profile_key=profile)
454 566
455 # job done, we can end the session 567 # job done, we can end the session
456 form = data_form.Form('form', title=_(u'Updated')) 568 form = data_form.Form("form", title=_(u"Updated"))
457 form.addField(data_form.Field('fixed', u'Status updated')) 569 form.addField(data_form.Field("fixed", u"Status updated"))
458 status = XEP_0050.STATUS.COMPLETED 570 status = XEP_0050.STATUS.COMPLETED
459 payload = None 571 payload = None
460 note = (self.NOTE.INFO, _(u"Status updated")) 572 note = (self.NOTE.INFO, _(u"Status updated"))
461 else: 573 else:
462 raise AdHocError(XEP_0050.ERROR.INTERNAL) 574 raise AdHocError(XEP_0050.ERROR.INTERNAL)
463 575
464 return (payload, status, None, note) 576 return (payload, status, None, note)
465 577
466 def _run(self, service_jid_s='', node='', profile_key=C.PROF_KEY_NONE): 578 def _run(self, service_jid_s="", node="", profile_key=C.PROF_KEY_NONE):
467 client = self.host.getClient(profile_key) 579 client = self.host.getClient(profile_key)
468 service_jid = jid.JID(service_jid_s) if service_jid_s else None 580 service_jid = jid.JID(service_jid_s) if service_jid_s else None
469 d = self.run(client, service_jid, node or None) 581 d = self.run(client, service_jid, node or None)
470 d.addCallback(lambda xmlui: xmlui.toXml()) 582 d.addCallback(lambda xmlui: xmlui.toXml())
471 return d 583 return d
481 @return(unicode): command page XMLUI 593 @return(unicode): command page XMLUI
482 """ 594 """
483 if service_jid is None: 595 if service_jid is None:
484 service_jid = jid.JID(client.jid.host) 596 service_jid = jid.JID(client.jid.host)
485 session_id, session_data = self.requesting.newSession(profile=client.profile) 597 session_id, session_data = self.requesting.newSession(profile=client.profile)
486 session_data['jid'] = service_jid 598 session_data["jid"] = service_jid
487 if node is None: 599 if node is None:
488 xmlui = yield self.list(client, service_jid) 600 xmlui = yield self.list(client, service_jid)
489 else: 601 else:
490 session_data['node'] = node 602 session_data["node"] = node
491 cb_data = yield self.requestingEntity({'session_id': session_id}, client.profile) 603 cb_data = yield self.requestingEntity(
492 xmlui = cb_data['xmlui'] 604 {"session_id": session_id}, client.profile
605 )
606 xmlui = cb_data["xmlui"]
493 607
494 xmlui.session_id = session_id 608 xmlui.session_id = session_id
495 defer.returnValue(xmlui) 609 defer.returnValue(xmlui)
496 610
497 def _list(self, to_jid_s, profile_key): 611 def _list(self, to_jid_s, profile_key):
510 """ 624 """
511 d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) 625 d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS)
512 d.addCallback(self._items2XMLUI, no_instructions) 626 d.addCallback(self._items2XMLUI, no_instructions)
513 return d 627 return d
514 628
515 def addAdHocCommand(self, callback, label, node=None, features=None, timeout=600, allowed_jids=None, allowed_groups=None, 629 def addAdHocCommand(
516 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, profile_key=C.PROF_KEY_NONE): 630 self,
631 callback,
632 label,
633 node=None,
634 features=None,
635 timeout=600,
636 allowed_jids=None,
637 allowed_groups=None,
638 allowed_magics=None,
639 forbidden_jids=None,
640 forbidden_groups=None,
641 profile_key=C.PROF_KEY_NONE,
642 ):
517 """Add an ad-hoc command for the current profile 643 """Add an ad-hoc command for the current profile
518 644
519 @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred 645 @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred
520 @param label: label associated with this command on the main menu 646 @param label: label associated with this command on the main menu
521 @param node: disco item node associated with this command. None to use autogenerated node 647 @param node: disco item node associated with this command. None to use autogenerated node
532 @return: node of the added command, useful to remove the command later 658 @return: node of the added command, useful to remove the command later
533 """ 659 """
534 # FIXME: "@ALL@" for profile_key seems useless and dangerous 660 # FIXME: "@ALL@" for profile_key seems useless and dangerous
535 661
536 if node is None: 662 if node is None:
537 node = "%s_%s" % ('COMMANDS', uuid4()) 663 node = "%s_%s" % ("COMMANDS", uuid4())
538 664
539 if features is None: 665 if features is None:
540 features = [data_form.NS_X_DATA] 666 features = [data_form.NS_X_DATA]
541 667
542 if allowed_jids is None: 668 if allowed_jids is None:
543 allowed_jids = [] 669 allowed_jids = []
544 if allowed_groups is None: 670 if allowed_groups is None:
545 allowed_groups = [] 671 allowed_groups = []
546 if allowed_magics is None: 672 if allowed_magics is None:
547 allowed_magics = ['@PROFILE_BAREJID@'] 673 allowed_magics = ["@PROFILE_BAREJID@"]
548 if forbidden_jids is None: 674 if forbidden_jids is None:
549 forbidden_jids = [] 675 forbidden_jids = []
550 if forbidden_groups is None: 676 if forbidden_groups is None:
551 forbidden_groups = [] 677 forbidden_groups = []
552 678
553 for client in self.host.getClients(profile_key): 679 for client in self.host.getClients(profile_key):
554 #TODO: manage newly created/removed profiles 680 # TODO: manage newly created/removed profiles
555 _allowed_jids = (allowed_jids + [client.jid.userhostJID()]) if '@PROFILE_BAREJID@' in allowed_magics else allowed_jids 681 _allowed_jids = (
556 ad_hoc_command = AdHocCommand(self, callback, label, node, features, timeout, _allowed_jids, 682 (allowed_jids + [client.jid.userhostJID()])
557 allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client) 683 if "@PROFILE_BAREJID@" in allowed_magics
684 else allowed_jids
685 )
686 ad_hoc_command = AdHocCommand(
687 self,
688 callback,
689 label,
690 node,
691 features,
692 timeout,
693 _allowed_jids,
694 allowed_groups,
695 allowed_magics,
696 forbidden_jids,
697 forbidden_groups,
698 client,
699 )
558 ad_hoc_command.setHandlerParent(client) 700 ad_hoc_command.setHandlerParent(client)
559 profile_commands = self.answering.setdefault(client.profile, {}) 701 profile_commands = self.answering.setdefault(client.profile, {})
560 profile_commands[node] = ad_hoc_command 702 profile_commands[node] = ad_hoc_command
561 703
562 def onCmdRequest(self, request, profile): 704 def onCmdRequest(self, request, profile):
563 request.handled = True 705 request.handled = True
564 requestor = jid.JID(request['from']) 706 requestor = jid.JID(request["from"])
565 command_elt = request.elements(NS_COMMANDS, 'command').next() 707 command_elt = request.elements(NS_COMMANDS, "command").next()
566 action = command_elt.getAttribute('action', self.ACTION.EXECUTE) 708 action = command_elt.getAttribute("action", self.ACTION.EXECUTE)
567 node = command_elt.getAttribute('node') 709 node = command_elt.getAttribute("node")
568 if not node: 710 if not node:
569 raise exceptions.DataError 711 raise exceptions.DataError
570 sessionid = command_elt.getAttribute('sessionid') 712 sessionid = command_elt.getAttribute("sessionid")
571 try: 713 try:
572 command = self.answering[profile][node] 714 command = self.answering[profile][node]
573 except KeyError: 715 except KeyError:
574 raise exceptions.DataError 716 raise exceptions.DataError
575 command.onRequest(command_elt, requestor, action, sessionid) 717 command.onRequest(command_elt, requestor, action, sessionid)
580 722
581 def __init__(self, plugin_parent): 723 def __init__(self, plugin_parent):
582 self.plugin_parent = plugin_parent 724 self.plugin_parent = plugin_parent
583 725
584 def connectionInitialized(self): 726 def connectionInitialized(self):
585 self.xmlstream.addObserver(CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile) 727 self.xmlstream.addObserver(
586 728 CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile
587 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 729 )
730
731 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
588 identities = [] 732 identities = []
589 if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get(self.parent.profile): # we only add the identity if we have registred commands 733 if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get(
734 self.parent.profile
735 ): # we only add the identity if we have registred commands
590 identities.append(ID_CMD_LIST) 736 identities.append(ID_CMD_LIST)
591 return [disco.DiscoFeature(NS_COMMANDS)] + identities 737 return [disco.DiscoFeature(NS_COMMANDS)] + identities
592 738
593 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 739 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
594 ret = [] 740 ret = []
595 if nodeIdentifier == NS_COMMANDS: 741 if nodeIdentifier == NS_COMMANDS:
596 for command in self.plugin_parent.answering.get(self.parent.profile,{}).values(): 742 for command in self.plugin_parent.answering.get(
743 self.parent.profile, {}
744 ).values():
597 if command.isAuthorised(requestor): 745 if command.isAuthorised(requestor):
598 ret.append(disco.DiscoItem(self.parent.jid, command.node, command.getName())) #TODO: manage name language 746 ret.append(
747 disco.DiscoItem(self.parent.jid, command.node, command.getName())
748 ) # TODO: manage name language
599 return ret 749 return ret