comparison sat_frontends/jp/base.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/jp/base.py@340128e0b354
children 4011e4ee3151
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # jp: a SAT command line tool
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21
22 ### logging ###
23 import logging as log
24 log.basicConfig(level=log.DEBUG,
25 format='%(message)s')
26 ###
27
28 import sys
29 import locale
30 import os.path
31 import argparse
32 from glob import iglob
33 from importlib import import_module
34 from sat_frontends.tools.jid import JID
35 from sat.tools import config
36 from sat.tools.common import dynamic_import
37 from sat.tools.common import uri
38 from sat.core import exceptions
39 import sat_frontends.jp
40 from sat_frontends.jp.constants import Const as C
41 from sat_frontends.tools import misc
42 import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI
43 import shlex
44 from collections import OrderedDict
45
46 ## bridge handling
47 # we get bridge name from conf and initialise the right class accordingly
48 main_config = config.parseMainConf()
49 bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')
50
51
52 # TODO: move loops handling in a separated module
53 if 'dbus' in bridge_name:
54 from gi.repository import GLib
55
56
57 class JPLoop(object):
58
59 def __init__(self):
60 self.loop = GLib.MainLoop()
61
62 def run(self):
63 self.loop.run()
64
65 def quit(self):
66 self.loop.quit()
67
68 def call_later(self, delay, callback, *args):
69 """call a callback repeatedly
70
71 @param delay(int): delay between calls in ms
72 @param callback(callable): method to call
73 if the callback return True, the call will continue
74 else the calls will stop
75 @param *args: args of the callbac
76 """
77 GLib.timeout_add(delay, callback, *args)
78
79 else:
80 print u"can't start jp: only D-Bus bridge is currently handled"
81 sys.exit(C.EXIT_ERROR)
82 # FIXME: twisted loop can be used when jp can handle fully async bridges
83 # from twisted.internet import reactor
84
85 # class JPLoop(object):
86
87 # def run(self):
88 # reactor.run()
89
90 # def quit(self):
91 # reactor.stop()
92
93 # def _timeout_cb(self, args, callback, delay):
94 # ret = callback(*args)
95 # if ret:
96 # reactor.callLater(delay, self._timeout_cb, args, callback, delay)
97
98 # def call_later(self, delay, callback, *args):
99 # delay = float(delay) / 1000
100 # reactor.callLater(delay, self._timeout_cb, args, callback, delay)
101
102 if bridge_name == "embedded":
103 from sat.core import sat_main
104 sat = sat_main.SAT()
105
106 if sys.version_info < (2, 7, 3):
107 # XXX: shlex.split only handle unicode since python 2.7.3
108 # this is a workaround for older versions
109 old_split = shlex.split
110 new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)]
111 if isinstance(s, unicode) else old_split(s, *a, **kw))
112 shlex.split = new_split
113
114 try:
115 import progressbar
116 except ImportError:
117 msg = (_(u'ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') +
118 _(u'Progress bar deactivated\n--\n'))
119 print >>sys.stderr,msg.encode('utf-8')
120 progressbar=None
121
122 #consts
123 PROG_NAME = u"jp"
124 DESCRIPTION = """This software is a command line tool for XMPP.
125 Get the latest version at """ + C.APP_URL
126
127 COPYLEFT = u"""Copyright (C) 2009-2018 Jérôme Poisson, Adrien Cossa
128 This program comes with ABSOLUTELY NO WARRANTY;
129 This is free software, and you are welcome to redistribute it under certain conditions.
130 """
131
132 PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms
133
134
135 def unicode_decoder(arg):
136 # Needed to have unicode strings from arguments
137 return arg.decode(locale.getpreferredencoding())
138
139
140 class Jp(object):
141 """
142 This class can be use to establish a connection with the
143 bridge. Moreover, it should manage a main loop.
144
145 To use it, you mainly have to redefine the method run to perform
146 specify what kind of operation you want to perform.
147
148 """
149 def __init__(self):
150 """
151
152 @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting,
153 or if you want the user to stop by himself
154 @attribute progress_success(callable): method to call when progress just started
155 by default display a message
156 @attribute progress_success(callable): method to call when progress is successfully finished
157 by default display a message
158 @attribute progress_failure(callable): method to call when progress failed
159 by default display a message
160 """
161 # FIXME: need_loop should be removed, everything must be async in bridge so
162 # loop will always be needed
163 bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
164 if bridge_module is None:
165 log.error(u"Can't import {} bridge".format(bridge_name))
166 sys.exit(1)
167
168 self.bridge = bridge_module.Bridge()
169 self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)
170
171 def _bridgeCb(self):
172 self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
173 description=DESCRIPTION)
174 self._make_parents()
175 self.add_parser_options()
176 self.subparsers = self.parser.add_subparsers(title=_(u'Available commands'), dest='subparser_name')
177 self._auto_loop = False # when loop is used for internal reasons
178 self._need_loop = False
179
180 # progress attributes
181 self._progress_id = None # TODO: manage several progress ids
182 self.quit_on_progress_end = True
183
184 # outputs
185 self._outputs = {}
186 for type_ in C.OUTPUT_TYPES:
187 self._outputs[type_] = OrderedDict()
188 self.default_output = {}
189
190 def _bridgeEb(self, failure):
191 if isinstance(failure, exceptions.BridgeExceptionNoService):
192 print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
193 elif isinstance(failure, exceptions.BridgeInitError):
194 print(_(u"Can't init bridge"))
195 else:
196 print(_(u"Error while initialising bridge: {}".format(failure)))
197 sys.exit(C.EXIT_BRIDGE_ERROR)
198
199 @property
200 def version(self):
201 return self.bridge.getVersion()
202
203 @property
204 def progress_id(self):
205 return self._progress_id
206
207 @progress_id.setter
208 def progress_id(self, value):
209 self._progress_id = value
210 self.replayCache('progress_ids_cache')
211
212 @property
213 def watch_progress(self):
214 try:
215 self.pbar
216 except AttributeError:
217 return False
218 else:
219 return True
220
221 @watch_progress.setter
222 def watch_progress(self, watch_progress):
223 if watch_progress:
224 self.pbar = None
225
226 @property
227 def verbosity(self):
228 try:
229 return self.args.verbose
230 except AttributeError:
231 return 0
232
233 def replayCache(self, cache_attribute):
234 """Replay cached signals
235
236 @param cache_attribute(str): name of the attribute containing the cache
237 if the attribute doesn't exist, there is no cache and the call is ignored
238 else the cache must be a list of tuples containing the replay callback as first item,
239 then the arguments to use
240 """
241 try:
242 cache = getattr(self, cache_attribute)
243 except AttributeError:
244 pass
245 else:
246 for cache_data in cache:
247 cache_data[0](*cache_data[1:])
248
249 def disp(self, msg, verbosity=0, error=False, no_lf=False):
250 """Print a message to user
251
252 @param msg(unicode): message to print
253 @param verbosity(int): minimal verbosity to display the message
254 @param error(bool): if True, print to stderr instead of stdout
255 @param no_lf(bool): if True, do not emit line feed at the end of line
256 """
257 if self.verbosity >= verbosity:
258 if error:
259 if no_lf:
260 print >>sys.stderr,msg.encode('utf-8'),
261 else:
262 print >>sys.stderr,msg.encode('utf-8')
263 else:
264 if no_lf:
265 print msg.encode('utf-8'),
266 else:
267 print msg.encode('utf-8')
268
269 def output(self, type_, name, extra_outputs, data):
270 if name in extra_outputs:
271 extra_outputs[name](data)
272 else:
273 self._outputs[type_][name]['callback'](data)
274
275 def addOnQuitCallback(self, callback, *args, **kwargs):
276 """Add a callback which will be called on quit command
277
278 @param callback(callback): method to call
279 """
280 try:
281 callbacks_list = self._onQuitCallbacks
282 except AttributeError:
283 callbacks_list = self._onQuitCallbacks = []
284 finally:
285 callbacks_list.append((callback, args, kwargs))
286
287 def getOutputChoices(self, output_type):
288 """Return valid output filters for output_type
289
290 @param output_type: True for default,
291 else can be any registered type
292 """
293 return self._outputs[output_type].keys()
294
295 def _make_parents(self):
296 self.parents = {}
297
298 # we have a special case here as the start-session option is present only if connection is not needed,
299 # so we create two similar parents, one with the option, the other one without it
300 for parent_name in ('profile', 'profile_session'):
301 parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
302 parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
303 parent.add_argument("--pwd", action="store", type=unicode_decoder, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary"))
304
305 profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session']
306
307 connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else")
308 profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
309
310 profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group()
311 profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
312 profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting"))
313
314 progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
315 if progressbar:
316 progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar"))
317
318 verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
319 verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)"))
320
321 draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
322 draft_group = draft_parent.add_argument_group(_('draft handling'))
323 draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft"))
324 draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve"))
325
326
327 def make_pubsub_group(self, flags, defaults):
328 """generate pubsub options according to flags
329
330 @param flags(iterable[unicode]): see [CommandBase.__init__]
331 @param defaults(dict[unicode, unicode]): help text for default value
332 key can be "service" or "node"
333 value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT
334 @return (ArgumentParser): parser to add
335 """
336 flags = misc.FlagsHandler(flags)
337 parent = argparse.ArgumentParser(add_help=False)
338 pubsub_group = parent.add_argument_group('pubsub')
339 pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder,
340 help=_(u"Pubsub URL (xmpp or http)"))
341
342 service_help = _(u"JID of the PubSub service")
343 if not flags.service:
344 default = defaults.pop(u'service', _(u'PEP service'))
345 if default is not None:
346 service_help += _(u" (DEFAULT: {default})".format(default=default))
347 pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'',
348 help=service_help)
349
350 node_help = _(u"node to request")
351 if not flags.node:
352 default = defaults.pop(u'node', _(u'standard node'))
353 if default is not None:
354 node_help += _(u" (DEFAULT: {default})".format(default=default))
355 pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help)
356
357 if flags.single_item:
358 item_help = (u"item to retrieve")
359 if not flags.item:
360 default = defaults.pop(u'item', _(u'last item'))
361 if default is not None:
362 item_help += _(u" (DEFAULT: {default})".format(default=default))
363 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=item_help)
364 pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item'))
365 elif flags.multi_items:
366 # mutiple items
367 pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)"))
368 if not flags.no_max:
369 pubsub_group.add_argument("-m", "--max", type=int, default=10,
370 help=_(u"maximum number of items to get ({no_limit} to get all items)".format(no_limit=C.NO_LIMIT)))
371
372 if flags:
373 raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags)))
374 if defaults:
375 raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults))
376
377 return parent
378
379 def add_parser_options(self):
380 self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
381
382 def register_output(self, type_, name, callback, description="", default=False):
383 if type_ not in C.OUTPUT_TYPES:
384 log.error(u"Invalid output type {}".format(type_))
385 return
386 self._outputs[type_][name] = {'callback': callback,
387 'description': description
388 }
389 if default:
390 if type_ in self.default_output:
391 self.disp(_(u'there is already a default output for {}, ignoring new one').format(type_))
392 else:
393 self.default_output[type_] = name
394
395
396 def parse_output_options(self):
397 options = self.command.args.output_opts
398 options_dict = {}
399 for option in options:
400 try:
401 key, value = option.split(u'=', 1)
402 except ValueError:
403 key, value = option, None
404 options_dict[key.strip()] = value.strip() if value is not None else None
405 return options_dict
406
407 def check_output_options(self, accepted_set, options):
408 if not accepted_set.issuperset(options):
409 self.disp(u"The following output options are invalid: {invalid_options}".format(
410 invalid_options = u', '.join(set(options).difference(accepted_set))),
411 error=True)
412 self.quit(C.EXIT_BAD_ARG)
413
414 def import_plugins(self):
415 """Automaticaly import commands and outputs in jp
416
417 looks from modules names cmd_*.py in jp path and import them
418 """
419 path = os.path.dirname(sat_frontends.jp.__file__)
420 # XXX: outputs must be imported before commands as they are used for arguments
421 for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')):
422 modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
423 for module_name in modules:
424 module_path = "sat_frontends.jp." + module_name
425 try:
426 module = import_module(module_path)
427 self.import_plugin_module(module, type_)
428 except ImportError as e:
429 self.disp(_(u"Can't import {module_path} plugin, ignoring it: {msg}".format(
430 module_path = module_path,
431 msg = e)), error=True)
432 except exceptions.CancelError:
433 continue
434 except exceptions.MissingModule as e:
435 self.disp(_(u"Missing module for plugin {name}: {missing}".format(
436 name = module_path,
437 missing = e)), error=True)
438
439
440 def import_plugin_module(self, module, type_):
441 """add commands or outpus from a module to jp
442
443 @param module: module containing commands or outputs
444 @param type_(str): one of C_PLUGIN_*
445 """
446 try:
447 class_names = getattr(module, '__{}__'.format(type_))
448 except AttributeError:
449 log.disp(_(u"Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True)
450 raise ImportError
451 else:
452 for class_name in class_names:
453 cls = getattr(module, class_name)
454 cls(self)
455
456 def get_xmpp_uri_from_http(self, http_url):
457 """parse HTML page at http(s) URL, and looks for xmpp: uri"""
458 if http_url.startswith('https'):
459 scheme = u'https'
460 elif http_url.startswith('http'):
461 scheme = u'http'
462 else:
463 raise exceptions.InternalError(u'An HTTP scheme is expected in this method')
464 self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
465 # HTTP URL, we try to find xmpp: links
466 try:
467 from lxml import etree
468 except ImportError:
469 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
470 self.host.quit(1)
471 import urllib2
472 parser = etree.HTMLParser()
473 try:
474 root = etree.parse(urllib2.urlopen(http_url), parser)
475 except etree.XMLSyntaxError as e:
476 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
477 links = []
478 else:
479 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
480 if not links:
481 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
482 self.host.quit(1)
483 xmpp_uri = links[0].get('href')
484 return xmpp_uri
485
486 def parse_pubsub_args(self):
487 if self.args.pubsub_url is not None:
488 url = self.args.pubsub_url
489
490 if url.startswith('http'):
491 # http(s) URL, we try to retrieve xmpp one from there
492 url = self.get_xmpp_uri_from_http(url)
493
494 try:
495 uri_data = uri.parseXMPPUri(url)
496 except ValueError:
497 self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url))
498 else:
499 if uri_data[u'type'] == 'pubsub':
500 # URL is alright, we only set data not already set by other options
501 if not self.args.service:
502 self.args.service = uri_data[u'path']
503 if not self.args.node:
504 self.args.node = uri_data[u'node']
505 uri_item = uri_data.get(u'item')
506 if uri_item:
507 # there is an item in URI
508 # we use it only if item is not already set
509 # and item_last is not used either
510 try:
511 item = self.args.item
512 except AttributeError:
513 if not self.args.items:
514 self.args.items = [uri_item]
515 else:
516 if not item:
517 try:
518 item_last = self.args.item_last
519 except AttributeError:
520 item_last = False
521 if not item_last:
522 self.args.item = uri_item
523 else:
524 self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url))
525 flags = self.args._cmd._pubsub_flags
526 # we check required arguments here instead of using add_arguments' required option
527 # because the required argument can be set in URL
528 if C.SERVICE in flags and not self.args.service:
529 self.parser.error(_(u"argument -s/--service is required"))
530 if C.NODE in flags and not self.args.node:
531 self.parser.error(_(u"argument -n/--node is required"))
532 if C.ITEM in flags and not self.args.item:
533 self.parser.error(_(u"argument -i/--item is required"))
534
535 # FIXME: mutually groups can't be nested in a group and don't support title
536 # so we check conflict here. This may be fixed in Python 3, to be checked
537 try:
538 if self.args.item and self.args.item_last:
539 self.parser.error(_(u"--item and --item-last can't be used at the same time"))
540 except AttributeError:
541 pass
542
543 def run(self, args=None, namespace=None):
544 self.args = self.parser.parse_args(args, namespace=None)
545 if self.args._cmd._use_pubsub:
546 self.parse_pubsub_args()
547 try:
548 self.args._cmd.run()
549 if self._need_loop or self._auto_loop:
550 self._start_loop()
551 except KeyboardInterrupt:
552 log.info(_("User interruption: good bye"))
553
554 def _start_loop(self):
555 self.loop = JPLoop()
556 self.loop.run()
557
558 def stop_loop(self):
559 try:
560 self.loop.quit()
561 except AttributeError:
562 pass
563
564 def confirmOrQuit(self, message, cancel_message=_(u"action cancelled by user")):
565 """Request user to confirm action, and quit if he doesn't"""
566
567 res = raw_input("{} (y/N)? ".format(message))
568 if res not in ("y", "Y"):
569 self.disp(cancel_message)
570 self.quit(C.EXIT_USER_CANCELLED)
571
572 def quitFromSignal(self, errcode=0):
573 """Same as self.quit, but from a signal handler
574
575 /!\: return must be used after calling this method !
576 """
577 assert self._need_loop
578 # XXX: python-dbus will show a traceback if we exit in a signal handler
579 # so we use this little timeout trick to avoid it
580 self.loop.call_later(0, self.quit, errcode)
581
582 def quit(self, errcode=0):
583 # first the onQuitCallbacks
584 try:
585 callbacks_list = self._onQuitCallbacks
586 except AttributeError:
587 pass
588 else:
589 for callback, args, kwargs in callbacks_list:
590 callback(*args, **kwargs)
591
592 self.stop_loop()
593 sys.exit(errcode)
594
595 def check_jids(self, jids):
596 """Check jids validity, transform roster name to corresponding jids
597
598 @param profile: profile name
599 @param jids: list of jids
600 @return: List of jids
601
602 """
603 names2jid = {}
604 nodes2jid = {}
605
606 for contact in self.bridge.getContacts(self.profile):
607 jid_s, attr, groups = contact
608 _jid = JID(jid_s)
609 try:
610 names2jid[attr["name"].lower()] = jid_s
611 except KeyError:
612 pass
613
614 if _jid.node:
615 nodes2jid[_jid.node.lower()] = jid_s
616
617 def expand_jid(jid):
618 _jid = jid.lower()
619 if _jid in names2jid:
620 expanded = names2jid[_jid]
621 elif _jid in nodes2jid:
622 expanded = nodes2jid[_jid]
623 else:
624 expanded = jid
625 return expanded.decode('utf-8')
626
627 def check(jid):
628 if not jid.is_valid:
629 log.error (_("%s is not a valid JID !"), jid)
630 self.quit(1)
631
632 dest_jids=[]
633 try:
634 for i in range(len(jids)):
635 dest_jids.append(expand_jid(jids[i]))
636 check(dest_jids[i])
637 except AttributeError:
638 pass
639
640 return dest_jids
641
642 def connect_profile(self, callback):
643 """ Check if the profile is connected and do it if requested
644
645 @param callback: method to call when profile is connected
646 @exit: - 1 when profile is not connected and --connect is not set
647 - 1 when the profile doesn't exists
648 - 1 when there is a connection error
649 """
650 # FIXME: need better exit codes
651
652 def cant_connect(failure):
653 log.error(_(u"Can't connect profile: {reason}").format(reason=failure))
654 self.quit(1)
655
656 def cant_start_session(failure):
657 log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure))
658 self.quit(1)
659
660 self.profile = self.bridge.profileNameGet(self.args.profile)
661
662 if not self.profile:
663 log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile))
664 self.quit(1)
665
666 try:
667 start_session = self.args.start_session
668 except AttributeError:
669 pass
670 else:
671 if start_session:
672 self.bridge.profileStartSession(self.args.pwd, self.profile, lambda dummy: callback(), cant_start_session)
673 self._auto_loop = True
674 return
675 elif not self.bridge.profileIsSessionStarted(self.profile):
676 if not self.args.connect:
677 log.error(_(u"Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile))
678 self.quit(1)
679 else:
680 callback()
681 return
682
683
684 if not hasattr(self.args, 'connect'):
685 # a profile can be present without connect option (e.g. on profile creation/deletion)
686 return
687 elif self.args.connect is True: # if connection is asked, we connect the profile
688 self.bridge.connect(self.profile, self.args.pwd, {}, lambda dummy: callback(), cant_connect)
689 self._auto_loop = True
690 return
691 else:
692 if not self.bridge.isConnected(self.profile):
693 log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile))
694 self.quit(1)
695
696 callback()
697
698 def get_full_jid(self, param_jid):
699 """Return the full jid if possible (add main resource when find a bare jid)"""
700 _jid = JID(param_jid)
701 if not _jid.resource:
702 #if the resource is not given, we try to add the main resource
703 main_resource = self.bridge.getMainResource(param_jid, self.profile)
704 if main_resource:
705 return "%s/%s" % (_jid.bare, main_resource)
706 return param_jid
707
708
709 class CommandBase(object):
710
711 def __init__(self, host, name, use_profile=True, use_output=False, extra_outputs=None,
712 need_connect=None, help=None, **kwargs):
713 """Initialise CommandBase
714
715 @param host: Jp instance
716 @param name(unicode): name of the new command
717 @param use_profile(bool): if True, add profile selection/connection commands
718 @param use_output(bool, unicode): if not False, add --output option
719 @param extra_outputs(dict): list of command specific outputs:
720 key is output name ("default" to use as main output)
721 value is a callable which will format the output (data will be used as only argument)
722 if a key already exists with normal outputs, the extra one will be used
723 @param need_connect(bool, None): True if profile connection is needed
724 False else (profile session must still be started)
725 None to set auto value (i.e. True if use_profile is set)
726 Can't be set if use_profile is False
727 @param help(unicode): help message to display
728 @param **kwargs: args passed to ArgumentParser
729 use_* are handled directly, they can be:
730 - use_progress(bool): if True, add progress bar activation option
731 progress* signals will be handled
732 - use_verbose(bool): if True, add verbosity option
733 - use_pubsub(bool): if True, add pubsub options
734 mandatory arguments are controlled by pubsub_req
735 - use_draft(bool): if True, add draft handling options
736 ** other arguments **
737 - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be:
738 C.SERVICE: service is required
739 C.NODE: node is required
740 C.SINGLE_ITEM: only one item is allowed
741 @attribute need_loop(bool): to set by commands when loop is needed
742 """
743 self.need_loop = False # to be set by commands when loop is needed
744 try: # If we have subcommands, host is a CommandBase and we need to use host.host
745 self.host = host.host
746 except AttributeError:
747 self.host = host
748
749 # --profile option
750 parents = kwargs.setdefault('parents', set())
751 if use_profile:
752 #self.host.parents['profile'] is an ArgumentParser with profile connection arguments
753 if need_connect is None:
754 need_connect = True
755 parents.add(self.host.parents['profile' if need_connect else 'profile_session'])
756 else:
757 assert need_connect is None
758 self.need_connect = need_connect
759 # from this point, self.need_connect is None if connection is not needed at all
760 # False if session starting is needed, and True if full connection is needed
761
762 # --output option
763 if use_output:
764 if extra_outputs is None:
765 extra_outputs = {}
766 self.extra_outputs = extra_outputs
767 if use_output == True:
768 use_output = C.OUTPUT_TEXT
769 assert use_output in C.OUTPUT_TYPES
770 self._output_type = use_output
771 output_parent = argparse.ArgumentParser(add_help=False)
772 choices = set(self.host.getOutputChoices(use_output))
773 choices.update(extra_outputs)
774 if not choices:
775 raise exceptions.InternalError("No choice found for {} output type".format(use_output))
776 try:
777 default = self.host.default_output[use_output]
778 except KeyError:
779 if u'default' in choices:
780 default = u'default'
781 elif u'simple' in choices:
782 default = u'simple'
783 else:
784 default = list(choices)[0]
785 output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_(u"select output format (default: {})".format(default)))
786 output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option"))
787 parents.add(output_parent)
788 else:
789 assert extra_outputs is None
790
791 self._use_pubsub = kwargs.pop('use_pubsub', False)
792 if self._use_pubsub:
793 flags = kwargs.pop('pubsub_flags', [])
794 defaults = kwargs.pop('pubsub_defaults', {})
795 parents.add(self.host.make_pubsub_group(flags, defaults))
796 self._pubsub_flags = flags
797
798 # other common options
799 use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
800 for param, do_use in use_opts.iteritems():
801 opt=param[4:] # if param is use_verbose, opt is verbose
802 if opt not in self.host.parents:
803 raise exceptions.InternalError(u"Unknown parent option {}".format(opt))
804 del kwargs[param]
805 if do_use:
806 parents.add(self.host.parents[opt])
807
808 self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
809 if hasattr(self, "subcommands"):
810 self.subparsers = self.parser.add_subparsers()
811 else:
812 self.parser.set_defaults(_cmd=self)
813 self.add_parser_options()
814
815 @property
816 def args(self):
817 return self.host.args
818
819 @property
820 def profile(self):
821 return self.host.profile
822
823 @property
824 def verbosity(self):
825 return self.host.verbosity
826
827 @property
828 def progress_id(self):
829 return self.host.progress_id
830
831 @progress_id.setter
832 def progress_id(self, value):
833 self.host.progress_id = value
834
835 def progressStartedHandler(self, uid, metadata, profile):
836 if profile != self.profile:
837 return
838 if self.progress_id is None:
839 # the progress started message can be received before the id
840 # so we keep progressStarted signals in cache to replay they
841 # when the progress_id is received
842 cache_data = (self.progressStartedHandler, uid, metadata, profile)
843 try:
844 self.host.progress_ids_cache.append(cache_data)
845 except AttributeError:
846 self.host.progress_ids_cache = [cache_data]
847 else:
848 if self.host.watch_progress and uid == self.progress_id:
849 self.onProgressStarted(metadata)
850 self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate)
851
852 def progressFinishedHandler(self, uid, metadata, profile):
853 if profile != self.profile:
854 return
855 if uid == self.progress_id:
856 try:
857 self.host.pbar.finish()
858 except AttributeError:
859 pass
860 self.onProgressFinished(metadata)
861 if self.host.quit_on_progress_end:
862 self.host.quitFromSignal()
863
864 def progressErrorHandler(self, uid, message, profile):
865 if profile != self.profile:
866 return
867 if uid == self.progress_id:
868 if self.args.progress:
869 self.disp('') # progress is not finished, so we skip a line
870 if self.host.quit_on_progress_end:
871 self.onProgressError(message)
872 self.host.quitFromSignal(1)
873
874 def progressUpdate(self):
875 """This method is continualy called to update the progress bar"""
876 data = self.host.bridge.progressGet(self.progress_id, self.profile)
877 if data:
878 try:
879 size = data['size']
880 except KeyError:
881 self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True)
882 return False
883 if self.host.pbar is None:
884 #first answer, we must construct the bar
885 self.host.pbar = progressbar.ProgressBar(max_value=int(size),
886 widgets=[_(u"Progress: "),progressbar.Percentage(),
887 " ",
888 progressbar.Bar(),
889 " ",
890 progressbar.FileTransferSpeed(),
891 " ",
892 progressbar.ETA()])
893 self.host.pbar.start()
894
895 self.host.pbar.update(int(data['position']))
896
897 elif self.host.pbar is not None:
898 return False
899
900 self.onProgressUpdate(data)
901
902 return True
903
904 def onProgressStarted(self, metadata):
905 """Called when progress has just started
906
907 can be overidden by a command
908 @param metadata(dict): metadata as sent by bridge.progressStarted
909 """
910 self.disp(_(u"Operation started"), 2)
911
912 def onProgressUpdate(self, metadata):
913 """Method called on each progress updata
914
915 can be overidden by a command to handle progress metadata
916 @para metadata(dict): metadata as returned by bridge.progressGet
917 """
918 pass
919
920 def onProgressFinished(self, metadata):
921 """Called when progress has just finished
922
923 can be overidden by a command
924 @param metadata(dict): metadata as sent by bridge.progressFinished
925 """
926 self.disp(_(u"Operation successfully finished"), 2)
927
928 def onProgressError(self, error_msg):
929 """Called when a progress failed
930
931 @param error_msg(unicode): error message as sent by bridge.progressError
932 """
933 self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True)
934
935 def disp(self, msg, verbosity=0, error=False, no_lf=False):
936 return self.host.disp(msg, verbosity, error, no_lf)
937
938 def output(self, data):
939 try:
940 output_type = self._output_type
941 except AttributeError:
942 raise exceptions.InternalError(_(u'trying to use output when use_output has not been set'))
943 return self.host.output(output_type, self.args.output, self.extra_outputs, data)
944
945 def exitCb(self, msg=None):
946 """generic callback for success
947
948 optionally print a message, and quit
949 msg(None, unicode): if not None, print this message
950 """
951 if msg is not None:
952 self.disp(msg)
953 self.host.quit(C.EXIT_OK)
954
955 def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR):
956 """generic callback for errbacks
957
958 display failure_ then quit with generic error
959 @param failure_: arguments returned by errback
960 @param msg(unicode, None): message template
961 use {} if you want to display failure message
962 @param exit_code(int): shell exit code
963 """
964 if msg is None:
965 msg = _(u"error: {}")
966 self.disp(msg.format(failure_), error=True)
967 self.host.quit(exit_code)
968
969 def add_parser_options(self):
970 try:
971 subcommands = self.subcommands
972 except AttributeError:
973 # We don't have subcommands, the class need to implements add_parser_options
974 raise NotImplementedError
975
976 # now we add subcommands to ourself
977 for cls in subcommands:
978 cls(self)
979
980 def run(self):
981 """this method is called when a command is actually run
982
983 It set stuff like progression callbacks and profile connection
984 You should not overide this method: you should call self.start instead
985 """
986 # we keep a reference to run command, it may be useful e.g. for outputs
987 self.host.command = self
988 # host._need_loop is set here from our current value and not before
989 # as the need_loop decision must be taken only by then running command
990 self.host._need_loop = self.need_loop
991
992 try:
993 show_progress = self.args.progress
994 except AttributeError:
995 # the command doesn't use progress bar
996 pass
997 else:
998 if show_progress:
999 self.host.watch_progress = True
1000 # we need to register the following signal even if we don't display the progress bar
1001 self.host.bridge.register_signal("progressStarted", self.progressStartedHandler)
1002 self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler)
1003 self.host.bridge.register_signal("progressError", self.progressErrorHandler)
1004
1005 if self.need_connect is not None:
1006 self.host.connect_profile(self.connected)
1007 else:
1008 self.start()
1009
1010 def connected(self):
1011 """this method is called when profile is connected (or session is started)
1012
1013 this method is only called when use_profile is True
1014 most of time you should override self.start instead of this method, but if loop
1015 if not always needed depending on your arguments, you may override this method,
1016 but don't forget to call the parent one (i.e. this one) after self.need_loop is set
1017 """
1018 if not self.need_loop:
1019 self.host.stop_loop()
1020 self.start()
1021
1022 def start(self):
1023 """This is the starting point of the command, this method should be overriden
1024
1025 at this point, profile are connected if needed
1026 """
1027 pass
1028
1029
1030 class CommandAnswering(CommandBase):
1031 """Specialised commands which answer to specific actions
1032
1033 to manage action_types answer,
1034 """
1035 action_callbacks = {} # XXX: set managed action types in a dict here:
1036 # key is the action_type, value is the callable
1037 # which will manage the answer. profile filtering is
1038 # already managed when callback is called
1039
1040 def __init__(self, *args, **kwargs):
1041 super(CommandAnswering, self).__init__(*args, **kwargs)
1042 self.need_loop = True
1043
1044 def onActionNew(self, action_data, action_id, security_limit, profile):
1045 if profile != self.profile:
1046 return
1047 try:
1048 action_type = action_data['meta_type']
1049 except KeyError:
1050 try:
1051 xml_ui = action_data["xmlui"]
1052 except KeyError:
1053 pass
1054 else:
1055 self.onXMLUI(xml_ui)
1056 else:
1057 try:
1058 callback = self.action_callbacks[action_type]
1059 except KeyError:
1060 pass
1061 else:
1062 callback(action_data, action_id, security_limit, profile)
1063
1064 def onXMLUI(self, xml_ui):
1065 """Display a dialog received from the backend.
1066
1067 @param xml_ui (unicode): dialog XML representation
1068 """
1069 # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
1070 # should be available in the future
1071 # TODO: XMLUI module
1072 ui = ET.fromstring(xml_ui.encode('utf-8'))
1073 dialog = ui.find("dialog")
1074 if dialog is not None:
1075 self.disp(dialog.findtext("message"), error=dialog.get("level") == "error")
1076
1077 def connected(self):
1078 """Auto reply to confirmations requests"""
1079 self.need_loop = True
1080 super(CommandAnswering, self).connected()
1081 self.host.bridge.register_signal("actionNew", self.onActionNew)
1082 actions = self.host.bridge.actionsGet(self.profile)
1083 for action_data, action_id, security_limit in actions:
1084 self.onActionNew(action_data, action_id, security_limit, self.profile)