comparison sat_frontends/jp/cmd_pubsub.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/cmd_pubsub.py@1d754bc14381
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # jp: a SàT 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
21 import base
22 from sat.core.i18n import _
23 from sat.core import exceptions
24 from sat_frontends.jp.constants import Const as C
25 from sat_frontends.jp import common
26 from sat_frontends.jp import arg_tools
27 from functools import partial
28 from sat.tools.common import uri
29 from sat.tools.common.ansi import ANSI as A
30 from sat_frontends.tools import jid, strings
31 import argparse
32 import os.path
33 import re
34 import subprocess
35 import sys
36
37 __commands__ = ["Pubsub"]
38
39 PUBSUB_TMP_DIR = u"pubsub"
40 PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema"
41 ALLOWED_SUBSCRIPTIONS_OWNER = ('subscribed', 'pending', 'none')
42
43 # TODO: need to split this class in several modules, plugin should handle subcommands
44
45
46 class NodeInfo(base.CommandBase):
47
48 def __init__(self, host):
49 base.CommandBase.__init__(self, host, 'info', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node configuration'))
50 self.need_loop=True
51
52 def add_parser_options(self):
53 self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys',
54 help=_(u"data key to filter"))
55
56 def removePrefix(self, key):
57 return key[7:] if key.startswith(u"pubsub#") else key
58
59 def filterKey(self, key):
60 return any((key == k or key == u'pubsub#' + k) for k in self.args.keys)
61
62 def psNodeConfigurationGetCb(self, config_dict):
63 key_filter = (lambda k: True) if not self.args.keys else self.filterKey
64 config_dict = {self.removePrefix(k):v for k,v in config_dict.iteritems() if key_filter(k)}
65 self.output(config_dict)
66 self.host.quit()
67
68 def psNodeConfigurationGetEb(self, failure_):
69 self.disp(u"can't get node configuration: {reason}".format(
70 reason=failure_), error=True)
71 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
72
73 def start(self):
74 self.host.bridge.psNodeConfigurationGet(
75 self.args.service,
76 self.args.node,
77 self.profile,
78 callback=self.psNodeConfigurationGetCb,
79 errback=self.psNodeConfigurationGetEb)
80
81
82 class NodeCreate(base.CommandBase):
83
84 def __init__(self, host):
85 base.CommandBase.__init__(self, host, 'create', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'create a node'))
86 self.need_loop=True
87
88 def add_parser_options(self):
89 self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
90 default=[], metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set"))
91 self.parser.add_argument("-F", "--full-prefix", action="store_true", help=_(u"don't prepend \"pubsub#\" prefix to field names"))
92
93 def psNodeCreateCb(self, node_id):
94 if self.host.verbosity:
95 announce = _(u'node created successfully: ')
96 else:
97 announce = u''
98 self.disp(announce + node_id)
99 self.host.quit()
100
101 def psNodeCreateEb(self, failure_):
102 self.disp(u"can't create: {reason}".format(
103 reason=failure_), error=True)
104 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
105
106 def start(self):
107 if not self.args.full_prefix:
108 options = {u'pubsub#' + k: v for k,v in self.args.fields}
109 else:
110 options = dict(self.args.fields)
111 self.host.bridge.psNodeCreate(
112 self.args.service,
113 self.args.node,
114 options,
115 self.profile,
116 callback=self.psNodeCreateCb,
117 errback=partial(self.errback,
118 msg=_(u"can't create node: {}"),
119 exit_code=C.EXIT_BRIDGE_ERRBACK))
120
121
122 class NodeDelete(base.CommandBase):
123
124 def __init__(self, host):
125 base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a node'))
126 self.need_loop=True
127
128 def add_parser_options(self):
129 self.parser.add_argument('-f', '--force', action='store_true', help=_(u'delete node without confirmation'))
130
131 def psNodeDeleteCb(self):
132 self.disp(_(u'node [{node}] deleted successfully').format(node=self.args.node))
133 self.host.quit()
134
135 def start(self):
136 if not self.args.force:
137 if not self.args.service:
138 message = _(u"Are you sure to delete pep node [{node_id}] ?").format(
139 node_id=self.args.node)
140 else:
141 message = _(u"Are you sure to delete node [{node_id}] on service [{service}] ?").format(
142 node_id=self.args.node, service=self.args.service)
143 self.host.confirmOrQuit(message, _(u"node deletion cancelled"))
144
145 self.host.bridge.psNodeDelete(
146 self.args.service,
147 self.args.node,
148 self.profile,
149 callback=self.psNodeDeleteCb,
150 errback=partial(self.errback,
151 msg=_(u"can't delete node: {}"),
152 exit_code=C.EXIT_BRIDGE_ERRBACK))
153
154
155 class NodeSet(base.CommandBase):
156
157 def __init__(self, host):
158 base.CommandBase.__init__(self, host, 'set', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set node configuration'))
159 self.need_loop=True
160
161 def add_parser_options(self):
162 self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
163 required=True, metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set (required)"))
164
165 def psNodeConfigurationSetCb(self):
166 self.disp(_(u'node configuration successful'), 1)
167 self.host.quit()
168
169 def psNodeConfigurationSetEb(self, failure_):
170 self.disp(u"can't set node configuration: {reason}".format(
171 reason=failure_), error=True)
172 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
173
174 def getKeyName(self, k):
175 if not k.startswith(u'pubsub#'):
176 return u'pubsub#' + k
177 else:
178 return k
179
180 def start(self):
181 self.host.bridge.psNodeConfigurationSet(
182 self.args.service,
183 self.args.node,
184 {self.getKeyName(k): v for k,v in self.args.fields},
185 self.profile,
186 callback=self.psNodeConfigurationSetCb,
187 errback=self.psNodeConfigurationSetEb)
188
189
190 class NodeAffiliationsGet(base.CommandBase):
191
192 def __init__(self, host):
193 base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node affiliations (for node owner)'))
194 self.need_loop=True
195
196 def add_parser_options(self):
197 pass
198
199 def psNodeAffiliationsGetCb(self, affiliations):
200 self.output(affiliations)
201 self.host.quit()
202
203 def psNodeAffiliationsGetEb(self, failure_):
204 self.disp(u"can't get node affiliations: {reason}".format(
205 reason=failure_), error=True)
206 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
207
208 def start(self):
209 self.host.bridge.psNodeAffiliationsGet(
210 self.args.service,
211 self.args.node,
212 self.profile,
213 callback=self.psNodeAffiliationsGetCb,
214 errback=self.psNodeAffiliationsGetEb)
215
216
217 class NodeAffiliationsSet(base.CommandBase):
218
219 def __init__(self, host):
220 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set affiliations (for node owner)'))
221 self.need_loop=True
222
223 def add_parser_options(self):
224 # XXX: we use optional argument syntax for a required one because list of list of 2 elements
225 # (uses to construct dicts) don't work with positional arguments
226 self.parser.add_argument("-a",
227 "--affiliation",
228 dest="affiliations",
229 metavar=('JID', 'AFFILIATION'),
230 required=True,
231 type=base.unicode_decoder,
232 action="append",
233 nargs=2,
234 help=_(u"entity/affiliation couple(s)"))
235
236 def psNodeAffiliationsSetCb(self):
237 self.disp(_(u"affiliations have been set"), 1)
238 self.host.quit()
239
240 def psNodeAffiliationsSetEb(self, failure_):
241 self.disp(u"can't set node affiliations: {reason}".format(
242 reason=failure_), error=True)
243 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
244
245 def start(self):
246 affiliations = dict(self.args.affiliations)
247 self.host.bridge.psNodeAffiliationsSet(
248 self.args.service,
249 self.args.node,
250 affiliations,
251 self.profile,
252 callback=self.psNodeAffiliationsSetCb,
253 errback=self.psNodeAffiliationsSetEb)
254
255
256 class NodeAffiliations(base.CommandBase):
257 subcommands = (NodeAffiliationsGet, NodeAffiliationsSet)
258
259 def __init__(self, host):
260 super(NodeAffiliations, self).__init__(host, 'affiliations', use_profile=False, help=_(u'set or retrieve node affiliations'))
261
262
263 class NodeSubscriptionsGet(base.CommandBase):
264
265 def __init__(self, host):
266 base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node subscriptions (for node owner)'))
267 self.need_loop=True
268
269 def add_parser_options(self):
270 pass
271
272 def psNodeSubscriptionsGetCb(self, subscriptions):
273 self.output(subscriptions)
274 self.host.quit()
275
276 def psNodeSubscriptionsGetEb(self, failure_):
277 self.disp(u"can't get node subscriptions: {reason}".format(
278 reason=failure_), error=True)
279 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
280
281 def start(self):
282 self.host.bridge.psNodeSubscriptionsGet(
283 self.args.service,
284 self.args.node,
285 self.profile,
286 callback=self.psNodeSubscriptionsGetCb,
287 errback=self.psNodeSubscriptionsGetEb)
288
289
290 class StoreSubscriptionAction(argparse.Action):
291 """Action which handle subscription parameter for owner
292
293 list is given by pairs: jid and subscription state
294 if subscription state is not specified, it default to "subscribed"
295 """
296
297 def __call__(self, parser, namespace, values, option_string):
298 dest_dict = getattr(namespace, self.dest)
299 while values:
300 jid_s = values.pop(0)
301 try:
302 subscription = values.pop(0)
303 except IndexError:
304 subscription = 'subscribed'
305 if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER:
306 parser.error(_(u"subscription must be one of {}").format(u', '.join(ALLOWED_SUBSCRIPTIONS_OWNER)))
307 dest_dict[jid_s] = subscription
308
309
310 class NodeSubscriptionsSet(base.CommandBase):
311
312 def __init__(self, host):
313 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/modify subscriptions (for node owner)'))
314 self.need_loop=True
315
316 def add_parser_options(self):
317 # XXX: we use optional argument syntax for a required one because list of list of 2 elements
318 # (uses to construct dicts) don't work with positional arguments
319 self.parser.add_argument("-S",
320 "--subscription",
321 dest="subscriptions",
322 default={},
323 nargs='+',
324 metavar=('JID [SUSBSCRIPTION]'),
325 required=True,
326 type=base.unicode_decoder,
327 action=StoreSubscriptionAction,
328 help=_(u"entity/subscription couple(s)"))
329
330 def psNodeSubscriptionsSetCb(self):
331 self.disp(_(u"subscriptions have been set"), 1)
332 self.host.quit()
333
334 def psNodeSubscriptionsSetEb(self, failure_):
335 self.disp(u"can't set node subscriptions: {reason}".format(
336 reason=failure_), error=True)
337 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
338
339 def start(self):
340 self.host.bridge.psNodeSubscriptionsSet(
341 self.args.service,
342 self.args.node,
343 self.args.subscriptions,
344 self.profile,
345 callback=self.psNodeSubscriptionsSetCb,
346 errback=self.psNodeSubscriptionsSetEb)
347
348
349 class NodeSubscriptions(base.CommandBase):
350 subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet)
351
352 def __init__(self, host):
353 super(NodeSubscriptions, self).__init__(host, 'subscriptions', use_profile=False, help=_(u'get or modify node subscriptions'))
354
355
356 class NodeSchemaSet(base.CommandBase):
357
358 def __init__(self, host):
359 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/replace a schema'))
360 self.need_loop = True
361
362 def add_parser_options(self):
363 self.parser.add_argument('schema', help=_(u"schema to set (must be XML)"))
364
365 def psSchemaSetCb(self):
366 self.disp(_(u'schema has been set'), 1)
367 self.host.quit()
368
369 def start(self):
370 self.host.bridge.psSchemaSet(
371 self.args.service,
372 self.args.node,
373 self.args.schema,
374 self.profile,
375 callback=self.psSchemaSetCb,
376 errback=partial(self.errback,
377 msg=_(u"can't set schema: {}"),
378 exit_code=C.EXIT_BRIDGE_ERRBACK))
379
380
381 class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
382 use_items=False
383
384 def __init__(self, host):
385 base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.NODE}, use_draft=True, use_verbose=True, help=_(u'edit a schema'))
386 common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR)
387 self.need_loop=True
388
389 def add_parser_options(self):
390 pass
391
392 def psSchemaSetCb(self):
393 self.disp(_(u'schema has been set'), 1)
394 self.host.quit()
395
396 def publish(self, schema):
397 self.host.bridge.psSchemaSet(
398 self.args.service,
399 self.args.node,
400 schema,
401 self.profile,
402 callback=self.psSchemaSetCb,
403 errback=partial(self.errback,
404 msg=_(u"can't set schema: {}"),
405 exit_code=C.EXIT_BRIDGE_ERRBACK))
406
407 def psSchemaGetCb(self, schema):
408 try:
409 from lxml import etree
410 except ImportError:
411 self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
412 self.host.quit(1)
413 content_file_obj, content_file_path = self.getTmpFile()
414 schema = schema.strip()
415 if schema:
416 parser = etree.XMLParser(remove_blank_text=True)
417 schema_elt = etree.fromstring(schema, parser)
418 content_file_obj.write(etree.tostring(schema_elt, encoding="utf-8", pretty_print=True))
419 content_file_obj.seek(0)
420 self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj)
421
422 def start(self):
423 self.host.bridge.psSchemaGet(
424 self.args.service,
425 self.args.node,
426 self.profile,
427 callback=self.psSchemaGetCb,
428 errback=partial(self.errback,
429 msg=_(u"can't edit schema: {}"),
430 exit_code=C.EXIT_BRIDGE_ERRBACK))
431
432
433 class NodeSchemaGet(base.CommandBase):
434
435 def __init__(self, host):
436 base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_XML, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'get schema'))
437 self.need_loop=True
438
439 def add_parser_options(self):
440 pass
441
442 def psSchemaGetCb(self, schema):
443 if not schema:
444 self.disp(_(u'no schema found'), 1)
445 self.host.quit(1)
446 self.output(schema)
447 self.host.quit()
448
449 def start(self):
450 self.host.bridge.psSchemaGet(
451 self.args.service,
452 self.args.node,
453 self.profile,
454 callback=self.psSchemaGetCb,
455 errback=partial(self.errback,
456 msg=_(u"can't get schema: {}"),
457 exit_code=C.EXIT_BRIDGE_ERRBACK))
458
459
460 class NodeSchema(base.CommandBase):
461 subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet)
462
463 def __init__(self, host):
464 super(NodeSchema, self).__init__(host, 'schema', use_profile=False, help=_(u"data schema manipulation"))
465
466
467 class Node(base.CommandBase):
468 subcommands = (NodeInfo, NodeCreate, NodeDelete, NodeSet, NodeAffiliations, NodeSubscriptions, NodeSchema)
469
470 def __init__(self, host):
471 super(Node, self).__init__(host, 'node', use_profile=False, help=_('node handling'))
472
473
474 class Set(base.CommandBase):
475
476 def __init__(self, host):
477 base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'publish a new item or update an existing one'))
478 self.need_loop=True
479
480 def add_parser_options(self):
481 self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'', help=_(u"id, URL of the item to update, keyword, or nothing for new item"))
482
483 def psItemsSendCb(self, published_id):
484 if published_id:
485 self.disp(u"Item published at {pub_id}".format(pub_id=published_id))
486 else:
487 self.disp(u"Item published")
488 self.host.quit(C.EXIT_OK)
489
490 def start(self):
491 try:
492 from lxml import etree
493 except ImportError:
494 self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
495 self.host.quit(1)
496 try:
497 element = etree.parse(sys.stdin).getroot()
498 except Exception as e:
499 self.parser.error(_(u"Can't parse the payload XML in input: {msg}").format(msg=e))
500 if element.tag in ('item', '{http://jabber.org/protocol/pubsub}item'):
501 if len(element) > 1:
502 self.parser.error(_(u"<item> can only have one child element (the payload)"))
503 element = element[0]
504 payload = etree.tostring(element, encoding='unicode')
505
506 self.host.bridge.psItemSend(self.args.service,
507 self.args.node,
508 payload,
509 self.args.item,
510 {},
511 self.profile,
512 callback=self.psItemsSendCb,
513 errback=partial(self.errback,
514 msg=_(u"can't send item: {}"),
515 exit_code=C.EXIT_BRIDGE_ERRBACK))
516
517
518 class Get(base.CommandBase):
519
520 def __init__(self, host):
521 base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_LIST_XML, use_pubsub=True, pubsub_flags={C.NODE, C.MULTI_ITEMS}, help=_(u'get pubsub item(s)'))
522 self.need_loop=True
523
524 def add_parser_options(self):
525 self.parser.add_argument("-S", "--sub-id", type=base.unicode_decoder, default=u'',
526 help=_(u"subscription id"))
527 # TODO: a key(s) argument to select keys to display
528 # TODO: add MAM filters
529
530
531 def psItemsGetCb(self, ps_result):
532 self.output(ps_result[0])
533 self.host.quit(C.EXIT_OK)
534
535 def psItemsGetEb(self, failure_):
536 self.disp(u"can't get pubsub items: {reason}".format(
537 reason=failure_), error=True)
538 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
539
540 def start(self):
541 self.host.bridge.psItemsGet(
542 self.args.service,
543 self.args.node,
544 self.args.max,
545 self.args.items,
546 self.args.sub_id,
547 {},
548 self.profile,
549 callback=self.psItemsGetCb,
550 errback=self.psItemsGetEb)
551
552 class Delete(base.CommandBase):
553
554 def __init__(self, host):
555 base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'delete an item'))
556 self.need_loop=True
557
558 def add_parser_options(self):
559 self.parser.add_argument("-f", "--force", action='store_true', help=_(u"delete without confirmation"))
560 self.parser.add_argument("-N", "--notify", action='store_true', help=_(u"notify deletion"))
561
562 def psItemsDeleteCb(self):
563 self.disp(_(u'item {item_id} has been deleted').format(item_id=self.args.item))
564 self.host.quit(C.EXIT_OK)
565
566 def start(self):
567 if not self.args.item:
568 self.parser.error(_(u"You need to specify an item to delete"))
569 if not self.args.force:
570 message = _(u"Are you sure to delete item {item_id} ?").format(item_id=self.args.item)
571 self.host.confirmOrQuit(message, _(u"item deletion cancelled"))
572 self.host.bridge.psRetractItem(
573 self.args.service,
574 self.args.node,
575 self.args.item,
576 self.args.notify,
577 self.profile,
578 callback=self.psItemsDeleteCb,
579 errback=partial(self.errback,
580 msg=_(u"can't delete item: {}"),
581 exit_code=C.EXIT_BRIDGE_ERRBACK))
582
583
584 class Edit(base.CommandBase, common.BaseEdit):
585
586 def __init__(self, host):
587 base.CommandBase.__init__(self, host, 'edit', use_verbose=True, use_pubsub=True,
588 pubsub_flags={C.NODE, C.SINGLE_ITEM}, use_draft=True, help=_(u'edit an existing or new pubsub item'))
589 common.BaseEdit.__init__(self, self.host, PUBSUB_TMP_DIR)
590
591 def add_parser_options(self):
592 pass
593
594 def edit(self, content_file_path, content_file_obj):
595 # we launch editor
596 self.runEditor("pubsub_editor_args", content_file_path, content_file_obj)
597
598 def publish(self, content):
599 published_id = self.host.bridge.psItemSend(self.pubsub_service, self.pubsub_node, content, self.pubsub_item or '', {}, self.profile)
600 if published_id:
601 self.disp(u"Item published at {pub_id}".format(pub_id=published_id))
602 else:
603 self.disp(u"Item published")
604
605 def getItemData(self, service, node, item):
606 try:
607 from lxml import etree
608 except ImportError:
609 self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
610 self.host.quit(1)
611 items = [item] if item is not None else []
612 item_raw = self.host.bridge.psItemsGet(service, node, 1, items, "", {}, self.profile)[0][0]
613 parser = etree.XMLParser(remove_blank_text=True)
614 item_elt = etree.fromstring(item_raw, parser)
615 item_id = item_elt.get('id')
616 try:
617 payload = item_elt[0]
618 except IndexError:
619 self.disp(_(u'Item has not payload'), 1)
620 return u''
621 return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id
622
623 def start(self):
624 self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj = self.getItemPath()
625 self.edit(content_file_path, content_file_obj)
626
627
628 class Subscribe(base.CommandBase):
629
630 def __init__(self, host):
631 base.CommandBase.__init__(self, host, 'subscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'subscribe to a node'))
632 self.need_loop=True
633
634 def add_parser_options(self):
635 pass
636
637 def psSubscribeCb(self, sub_id):
638 self.disp(_(u'subscription done'), 1)
639 if sub_id:
640 self.disp(_(u'subscription id: {sub_id}').format(sub_id=sub_id))
641 self.host.quit()
642
643 def start(self):
644 self.host.bridge.psSubscribe(
645 self.args.service,
646 self.args.node,
647 {},
648 self.profile,
649 callback=self.psSubscribeCb,
650 errback=partial(self.errback,
651 msg=_(u"can't subscribe to node: {}"),
652 exit_code=C.EXIT_BRIDGE_ERRBACK))
653
654
655 class Unsubscribe(base.CommandBase):
656 # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe
657
658 def __init__(self, host):
659 base.CommandBase.__init__(self, host, 'unsubscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'unsubscribe from a node'))
660 self.need_loop=True
661
662 def add_parser_options(self):
663 pass
664
665 def psUnsubscribeCb(self):
666 self.disp(_(u'subscription removed'), 1)
667 self.host.quit()
668
669 def start(self):
670 self.host.bridge.psUnsubscribe(
671 self.args.service,
672 self.args.node,
673 self.profile,
674 callback=self.psUnsubscribeCb,
675 errback=partial(self.errback,
676 msg=_(u"can't unsubscribe from node: {}"),
677 exit_code=C.EXIT_BRIDGE_ERRBACK))
678
679
680 class Subscriptions(base.CommandBase):
681
682 def __init__(self, host):
683 base.CommandBase.__init__(self, host, 'subscriptions', use_output=C.OUTPUT_LIST_DICT, use_pubsub=True, help=_(u'retrieve all subscriptions on a service'))
684 self.need_loop=True
685
686 def add_parser_options(self):
687 pass
688
689 def psSubscriptionsGetCb(self, subscriptions):
690 self.output(subscriptions)
691 self.host.quit()
692
693 def start(self):
694 self.host.bridge.psSubscriptionsGet(
695 self.args.service,
696 self.args.node,
697 self.profile,
698 callback=self.psSubscriptionsGetCb,
699 errback=partial(self.errback,
700 msg=_(u"can't retrieve subscriptions: {}"),
701 exit_code=C.EXIT_BRIDGE_ERRBACK))
702
703
704 class Affiliations(base.CommandBase):
705
706 def __init__(self, host):
707 base.CommandBase.__init__(self, host, 'affiliations', use_output=C.OUTPUT_DICT, use_pubsub=True, help=_(u'retrieve all affiliations on a service'))
708 self.need_loop=True
709
710 def add_parser_options(self):
711 pass
712
713 def psAffiliationsGetCb(self, affiliations):
714 self.output(affiliations)
715 self.host.quit()
716
717 def psAffiliationsGetEb(self, failure_):
718 self.disp(u"can't get node affiliations: {reason}".format(
719 reason=failure_), error=True)
720 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
721
722 def start(self):
723 self.host.bridge.psAffiliationsGet(
724 self.args.service,
725 self.args.node,
726 self.profile,
727 callback=self.psAffiliationsGetCb,
728 errback=self.psAffiliationsGetEb)
729
730
731 class Search(base.CommandBase):
732 """this command to a search without using MAM, i.e. by checking every items if dound by itself, so it may be heavy in resources both for server and client"""
733 RE_FLAGS = re.MULTILINE | re.UNICODE
734 EXEC_ACTIONS = (u'exec', u'external')
735
736 def __init__(self, host):
737 base.CommandBase.__init__(self, host, 'search', use_output=C.OUTPUT_XML, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS, C.NO_MAX},
738 use_verbose=True, help=_(u'search items corresponding to filters'))
739 self.need_loop=True
740
741 @property
742 def etree(self):
743 """load lxml.etree only if needed"""
744 if self._etree is None:
745 from lxml import etree
746 self._etree = etree
747 return self._etree
748
749 def filter_opt(self, value, type_):
750 value = base.unicode_decoder(value)
751 return (type_, value)
752
753 def filter_flag(self, value, type_):
754 value = C.bool(value)
755 return (type_, value)
756
757 def add_parser_options(self):
758 self.parser.add_argument("-D", "--max-depth", type=int, default=0, help=_(u"maximum depth of recursion (will search linked nodes if > 0, default: 0)"))
759 self.parser.add_argument("-m", "--max", type=int, default=30, help=_(u"maximum number of items to get per node ({} to get all items, default: 30)".format(C.NO_LIMIT)))
760 self.parser.add_argument("-N", "--namespace", action='append', nargs=2, default=[],
761 metavar="NAME NAMESPACE", help=_(u"namespace to use for xpath"))
762
763 # filters
764 filter_text = partial(self.filter_opt, type_=u'text')
765 filter_re = partial(self.filter_opt, type_=u'regex')
766 filter_xpath = partial(self.filter_opt, type_=u'xpath')
767 filter_python = partial(self.filter_opt, type_=u'python')
768 filters = self.parser.add_argument_group(_(u'filters'), _(u'only items corresponding to following filters will be kept'))
769 filters.add_argument("-t", "--text",
770 action='append', dest='filters', type=filter_text,
771 metavar='TEXT',
772 help=_(u"full text filter, item must contain this string (XML included)"))
773 filters.add_argument("-r", "--regex",
774 action='append', dest='filters', type=filter_re,
775 metavar='EXPRESSION',
776 help=_(u"like --text but using a regular expression"))
777 filters.add_argument("-x", "--xpath",
778 action='append', dest='filters', type=filter_xpath,
779 metavar='XPATH',
780 help=_(u"filter items which has elements matching this xpath"))
781 filters.add_argument("-P", "--python",
782 action='append', dest='filters', type=filter_python,
783 metavar='PYTHON_CODE',
784 help=_(u'Python expression which much return a bool (True to keep item, False to reject it). "item" is raw text item, "item_xml" is lxml\'s etree.Element'))
785
786 # filters flags
787 flag_case = partial(self.filter_flag, type_=u'ignore-case')
788 flag_invert = partial(self.filter_flag, type_=u'invert')
789 flag_dotall = partial(self.filter_flag, type_=u'dotall')
790 flag_matching = partial(self.filter_flag, type_=u'only-matching')
791 flags = self.parser.add_argument_group(_(u'filters flags'), _(u'filters modifiers (change behaviour of following filters)'))
792 flags.add_argument("-C", "--ignore-case",
793 action='append', dest='filters', type=flag_case,
794 const=('ignore-case', True), nargs='?',
795 metavar='BOOLEAN',
796 help=_(u"(don't) ignore case in following filters (default: case sensitive)"))
797 flags.add_argument("-I", "--invert",
798 action='append', dest='filters', type=flag_invert,
799 const=('invert', True), nargs='?',
800 metavar='BOOLEAN',
801 help=_(u"(don't) invert effect of following filters (default: don't invert)"))
802 flags.add_argument("-A", "--dot-all",
803 action='append', dest='filters', type=flag_dotall,
804 const=('dotall', True), nargs='?',
805 metavar='BOOLEAN',
806 help=_(u"(don't) use DOTALL option for regex (default: don't use)"))
807 flags.add_argument("-o", "--only-matching",
808 action='append', dest='filters', type=flag_matching,
809 const=('only-matching', True), nargs='?',
810 metavar='BOOLEAN',
811 help=_(u"keep only the matching part of the item"))
812
813 # action
814 self.parser.add_argument("action",
815 default="print",
816 nargs='?',
817 choices=('print', 'exec', 'external'),
818 help=_(u"action to do on found items (default: print)"))
819 self.parser.add_argument("command", nargs=argparse.REMAINDER)
820
821 def psItemsGetEb(self, failure_, service, node):
822 self.disp(u"can't get pubsub items at {service} (node: {node}): {reason}".format(
823 service=service,
824 node=node,
825 reason=failure_), error=True)
826 self.to_get -= 1
827
828 def getItems(self, depth, service, node, items):
829 search = partial(self.search, depth=depth)
830 errback = partial(self.psItemsGetEb, service=service, node=node)
831 self.host.bridge.psItemsGet(
832 service,
833 node,
834 self.args.max,
835 items,
836 "",
837 {},
838 self.profile,
839 callback=search,
840 errback=errback
841 )
842 self.to_get += 1
843
844 def _checkPubsubURL(self, match, found_nodes):
845 """check that the matched URL is an xmpp: one
846
847 @param found_nodes(list[unicode]): found_nodes
848 this list will be filled while xmpp: URIs are discovered
849 """
850 url = match.group(0)
851 if url.startswith(u'xmpp'):
852 try:
853 url_data = uri.parseXMPPUri(url)
854 except ValueError:
855 return
856 if url_data[u'type'] == u'pubsub':
857 found_node = {u'service': url_data[u'path'],
858 u'node': url_data[u'node']}
859 if u'item' in url_data:
860 found_node[u'item'] = url_data[u'item']
861 found_nodes.append(found_node)
862
863 def getSubNodes(self, item, depth):
864 """look for pubsub URIs in item, and getItems on the linked nodes"""
865 found_nodes = []
866 checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes)
867 strings.RE_URL.sub(checkURI, item)
868 for data in found_nodes:
869 self.getItems(depth+1,
870 data[u'service'],
871 data[u'node'],
872 [data[u'item']] if u'item' in data else []
873 )
874
875 def parseXml(self, item):
876 try:
877 return self.etree.fromstring(item)
878 except self.etree.XMLSyntaxError:
879 self.disp(_(u"item doesn't looks like XML, you have probably used --only-matching somewhere before and we have no more XML"), error=True)
880 self.host.quit(C.EXIT_BAD_ARG)
881
882 def filter(self, item):
883 """apply filters given on command line
884
885 if only-matching is used, item may be modified
886 @return (tuple[bool, unicode]): a tuple with:
887 - keep: True if item passed the filters
888 - item: it is returned in case of modifications
889 """
890 ignore_case = False
891 invert = False
892 dotall = False
893 only_matching = False
894 item_xml = None
895 for type_, value in self.args.filters:
896 keep = True
897
898 ## filters
899
900 if type_ == u'text':
901 if ignore_case:
902 if value.lower() not in item.lower():
903 keep = False
904 else:
905 if value not in item:
906 keep = False
907 if keep and only_matching:
908 # doesn't really make sens to keep a fixed string
909 # so we raise an error
910 self.host.disp(_(u"--only-matching used with fixed --text string, are you sure?"), error=True)
911 self.host.quit(C.EXIT_BAD_ARG)
912 elif type_ == u'regex':
913 flags = self.RE_FLAGS
914 if ignore_case:
915 flags |= re.IGNORECASE
916 if dotall:
917 flags |= re.DOTALL
918 match = re.search(value, item, flags)
919 keep = match != None
920 if keep and only_matching:
921 item = match.group()
922 item_xml = None
923 elif type_ == u'xpath':
924 if item_xml is None:
925 item_xml = self.parseXml(item)
926 try:
927 elts = item_xml.xpath(value, namespaces=self.args.namespace)
928 except self.etree.XPathEvalError as e:
929 self.disp(_(u"can't use xpath: {reason}").format(reason=e), error=True)
930 self.host.quit(C.EXIT_BAD_ARG)
931 keep = bool(elts)
932 if keep and only_matching:
933 item_xml = elts[0]
934 try:
935 item = self.etree.tostring(item_xml, encoding='unicode')
936 except TypeError:
937 # we have a string only, not an element
938 item = unicode(item_xml)
939 item_xml = None
940 elif type_ == u'python':
941 if item_xml is None:
942 item_xml = self.parseXml(item)
943 cmd_ns = {u'item': item,
944 u'item_xml': item_xml
945 }
946 try:
947 keep = eval(value, cmd_ns)
948 except SyntaxError as e:
949 self.disp(unicode(e), error=True)
950 self.host.quit(C.EXIT_BAD_ARG)
951
952 ## flags
953
954 elif type_ == u'ignore-case':
955 ignore_case = value
956 elif type_ == u'invert':
957 invert = value
958 # we need to continue, else loop would end here
959 continue
960 elif type_ == u'dotall':
961 dotall = value
962 elif type_ == u'only-matching':
963 only_matching = value
964 else:
965 raise exceptions.InternalError(_(u"unknown filter type {type}").format(type=type_))
966
967 if invert:
968 keep = not keep
969 if not keep:
970 return False, item
971
972 return True, item
973
974 def doItemAction(self, item, metadata):
975 """called when item has been kepts and the action need to be done
976
977 @param item(unicode): accepted item
978 """
979 action = self.args.action
980 if action == u'print' or self.host.verbosity > 0:
981 try:
982 self.output(item)
983 except self.etree.XMLSyntaxError:
984 # item is not valid XML, but a string
985 # can happen when --only-matching is used
986 self.disp(item)
987 if action in self.EXEC_ACTIONS:
988 item_elt = self.parseXml(item)
989 if action == u'exec':
990 use = {'service': metadata[u'service'],
991 'node': metadata[u'node'],
992 'item': item_elt.get('id'),
993 'profile': self.profile
994 }
995 # we need to send a copy of self.args.command
996 # else it would be modified
997 parser_args, use_args = arg_tools.get_use_args(self.host,
998 self.args.command,
999 use,
1000 verbose=self.host.verbosity > 1
1001 )
1002 cmd_args = sys.argv[0:1] + parser_args + use_args
1003 else:
1004 cmd_args = self.args.command
1005
1006
1007 self.disp(u'COMMAND: {command}'.format(
1008 command = u' '.join([arg_tools.escape(a) for a in cmd_args])), 2)
1009 if action == u'exec':
1010 ret = subprocess.call(cmd_args)
1011 else:
1012 p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE)
1013 p.communicate(item.encode('utf-8'))
1014 ret = p.wait()
1015 if ret != 0:
1016 self.disp(A.color(C.A_FAILURE, _(u"executed command failed with exit code {code}").format(code=ret)))
1017
1018 def search(self, items_data, depth):
1019 """callback of getItems
1020
1021 this method filters items, get sub nodes if needed,
1022 do the requested action, and exit the command when everything is done
1023 @param items_data(tuple): result of getItems
1024 @param depth(int): current depth level
1025 0 for first node, 1 for first children, and so on
1026 """
1027 items, metadata = items_data
1028 for item in items:
1029 if depth < self.args.max_depth:
1030 self.getSubNodes(item, depth)
1031 keep, item = self.filter(item)
1032 if not keep:
1033 continue
1034 self.doItemAction(item, metadata)
1035
1036 # we check if we got all getItems results
1037 self.to_get -= 1
1038 if self.to_get == 0:
1039 # yes, we can quit
1040 self.host.quit()
1041 assert self.to_get > 0
1042
1043 def start(self):
1044 if self.args.command:
1045 if self.args.action not in self.EXEC_ACTIONS:
1046 self.parser.error(_(u"Command can only be used with {actions} actions").format(
1047 actions=u', '.join(self.EXEC_ACTIONS)))
1048 else:
1049 if self.args.action in self.EXEC_ACTIONS:
1050 self.parser.error(_(u"you need to specify a command to execute"))
1051 if not self.args.node:
1052 # TODO: handle get service affiliations when node is not set
1053 self.parser.error(_(u"empty node is not handled yet"))
1054 # to_get is increased on each get and decreased on each answer
1055 # when it reach 0 again, the command is finished
1056 self.to_get = 0
1057 self._etree = None
1058 if self.args.filters is None:
1059 self.args.filters = []
1060 self.args.namespace = dict(self.args.namespace + [('pubsub', "http://jabber.org/protocol/pubsub")])
1061 self.getItems(0, self.args.service, self.args.node, self.args.items)
1062
1063
1064 class Uri(base.CommandBase):
1065
1066 def __init__(self, host):
1067 base.CommandBase.__init__(self, host, 'uri', use_profile=False, use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'build URI'))
1068 self.need_loop=True
1069
1070 def add_parser_options(self):
1071 self.parser.add_argument("-p", "--profile", type=base.unicode_decoder, default=C.PROF_KEY_DEFAULT, help=_(u"profile (used when no server is specified)"))
1072
1073 def display_uri(self, jid_):
1074 uri_args = {}
1075 if not self.args.service:
1076 self.args.service = jid.JID(jid_).bare
1077
1078 for key in ('node', 'service', 'item'):
1079 value = getattr(self.args, key)
1080 if key == 'service':
1081 key = 'path'
1082 if value:
1083 uri_args[key] = value
1084 self.disp(uri.buildXMPPUri(u'pubsub', **uri_args))
1085 self.host.quit()
1086
1087 def start(self):
1088 if not self.args.service:
1089 self.host.bridge.asyncGetParamA(
1090 u'JabberID',
1091 u'Connection',
1092 profile_key=self.args.profile,
1093 callback=self.display_uri,
1094 errback=partial(self.errback,
1095 msg=_(u"can't retrieve jid: {}"),
1096 exit_code=C.EXIT_BRIDGE_ERRBACK))
1097 else:
1098 self.display_uri(None)
1099
1100
1101 class HookCreate(base.CommandBase):
1102
1103 def __init__(self, host):
1104 base.CommandBase.__init__(self, host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'create a Pubsub hook'))
1105 self.need_loop=True
1106
1107 def add_parser_options(self):
1108 self.parser.add_argument('-t', '--type', default=u'python', choices=('python', 'python_file', 'python_code'), help=_(u"hook type"))
1109 self.parser.add_argument('-P', '--persistent', action='store_true', help=_(u"make hook persistent across restarts"))
1110 self.parser.add_argument("hook_arg", type=base.unicode_decoder, help=_(u"argument of the hook (depend of the type)"))
1111
1112 @staticmethod
1113 def checkArgs(self):
1114 if self.args.type == u'python_file':
1115 self.args.hook_arg = os.path.abspath(self.args.hook_arg)
1116 if not os.path.isfile(self.args.hook_arg):
1117 self.parser.error(_(u"{path} is not a file").format(path=self.args.hook_arg))
1118
1119 def start(self):
1120 self.checkArgs(self)
1121 self.host.bridge.psHookAdd(
1122 self.args.service,
1123 self.args.node,
1124 self.args.type,
1125 self.args.hook_arg,
1126 self.args.persistent,
1127 self.profile,
1128 callback=self.host.quit,
1129 errback=partial(self.errback,
1130 msg=_(u"can't create hook: {}"),
1131 exit_code=C.EXIT_BRIDGE_ERRBACK))
1132
1133
1134 class HookDelete(base.CommandBase):
1135
1136 def __init__(self, host):
1137 base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a Pubsub hook'))
1138 self.need_loop=True
1139
1140 def add_parser_options(self):
1141 self.parser.add_argument('-t', '--type', default=u'', choices=('', 'python', 'python_file', 'python_code'), help=_(u"hook type to remove, empty to remove all (default: remove all)"))
1142 self.parser.add_argument('-a', '--arg', dest='hook_arg', type=base.unicode_decoder, default=u'', help=_(u"argument of the hook to remove, empty to remove all (default: remove all)"))
1143
1144 def psHookRemoveCb(self, nb_deleted):
1145 self.disp(_(u'{nb_deleted} hook(s) have been deleted').format(
1146 nb_deleted = nb_deleted))
1147 self.host.quit()
1148
1149 def start(self):
1150 HookCreate.checkArgs(self)
1151 self.host.bridge.psHookRemove(
1152 self.args.service,
1153 self.args.node,
1154 self.args.type,
1155 self.args.hook_arg,
1156 self.profile,
1157 callback=self.psHookRemoveCb,
1158 errback=partial(self.errback,
1159 msg=_(u"can't delete hook: {}"),
1160 exit_code=C.EXIT_BRIDGE_ERRBACK))
1161
1162
1163 class HookList(base.CommandBase):
1164
1165 def __init__(self, host):
1166 base.CommandBase.__init__(self, host, 'list', use_output=C.OUTPUT_LIST_DICT, help=_(u'list hooks of a profile'))
1167 self.need_loop = True
1168
1169 def add_parser_options(self):
1170 pass
1171
1172 def psHookListCb(self, data):
1173 if not data:
1174 self.disp(_(u'No hook found.'))
1175 self.output(data)
1176 self.host.quit()
1177
1178 def start(self):
1179 self.host.bridge.psHookList(
1180 self.profile,
1181 callback=self.psHookListCb,
1182 errback=partial(self.errback,
1183 msg=_(u"can't list hooks: {}"),
1184 exit_code=C.EXIT_BRIDGE_ERRBACK))
1185
1186
1187 class Hook(base.CommandBase):
1188 subcommands = (HookCreate, HookDelete, HookList)
1189
1190 def __init__(self, host):
1191 super(Hook, self).__init__(host, 'hook', use_profile=False, use_verbose=True, help=_('trigger action on Pubsub notifications'))
1192
1193
1194 class Pubsub(base.CommandBase):
1195 subcommands = (Set, Get, Delete, Edit, Subscribe, Unsubscribe, Subscriptions, Node, Affiliations, Search, Hook, Uri)
1196
1197 def __init__(self, host):
1198 super(Pubsub, self).__init__(host, 'pubsub', use_profile=False, help=_('PubSub nodes/items management'))