Mercurial > libervia-backend
changeset 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 | bd30dc3ffe5a |
children | 9af003a8cb99 |
files | MANIFEST.in bin/sat frontends/src/__init__.py frontends/src/bridge/__init__.py frontends/src/bridge/bridge_frontend.py frontends/src/bridge/dbus_bridge.py frontends/src/jp/__init__.py frontends/src/jp/arg_tools.py frontends/src/jp/base.py frontends/src/jp/cmd_account.py frontends/src/jp/cmd_adhoc.py frontends/src/jp/cmd_avatar.py frontends/src/jp/cmd_blog.py frontends/src/jp/cmd_bookmarks.py frontends/src/jp/cmd_debug.py frontends/src/jp/cmd_event.py frontends/src/jp/cmd_file.py frontends/src/jp/cmd_forums.py frontends/src/jp/cmd_identity.py frontends/src/jp/cmd_info.py frontends/src/jp/cmd_input.py frontends/src/jp/cmd_invitation.py frontends/src/jp/cmd_merge_request.py frontends/src/jp/cmd_message.py frontends/src/jp/cmd_param.py frontends/src/jp/cmd_pipe.py frontends/src/jp/cmd_profile.py frontends/src/jp/cmd_pubsub.py frontends/src/jp/cmd_roster.py frontends/src/jp/cmd_shell.py frontends/src/jp/cmd_ticket.py frontends/src/jp/cmd_uri.py frontends/src/jp/common.py frontends/src/jp/constants.py frontends/src/jp/jp frontends/src/jp/output_std.py frontends/src/jp/output_template.py frontends/src/jp/output_xml.py frontends/src/jp/xmlui_manager.py frontends/src/primitivus/__init__.py frontends/src/primitivus/chat.py frontends/src/primitivus/config.py frontends/src/primitivus/constants.py frontends/src/primitivus/contact_list.py frontends/src/primitivus/game_tarot.py frontends/src/primitivus/keys.py frontends/src/primitivus/notify.py frontends/src/primitivus/primitivus frontends/src/primitivus/profile_manager.py frontends/src/primitivus/progress.py frontends/src/primitivus/status.py frontends/src/primitivus/widget.py frontends/src/primitivus/xmlui.py frontends/src/quick_frontend/__init__.py frontends/src/quick_frontend/constants.py frontends/src/quick_frontend/quick_app.py frontends/src/quick_frontend/quick_blog.py frontends/src/quick_frontend/quick_chat.py frontends/src/quick_frontend/quick_contact_list.py frontends/src/quick_frontend/quick_contact_management.py frontends/src/quick_frontend/quick_game_tarot.py frontends/src/quick_frontend/quick_games.py frontends/src/quick_frontend/quick_list_manager.py frontends/src/quick_frontend/quick_menus.py frontends/src/quick_frontend/quick_profile_manager.py frontends/src/quick_frontend/quick_utils.py frontends/src/quick_frontend/quick_widgets.py frontends/src/tools/__init__.py frontends/src/tools/composition.py frontends/src/tools/css_color.py frontends/src/tools/games.py frontends/src/tools/host_listener.py frontends/src/tools/jid.py frontends/src/tools/misc.py frontends/src/tools/strings.py frontends/src/tools/xmltools.py frontends/src/tools/xmlui.py sat/__init__.py sat/bridge/__init__.py sat/bridge/bridge_constructor/__init__.py sat/bridge/bridge_constructor/base_constructor.py sat/bridge/bridge_constructor/bridge_constructor.py sat/bridge/bridge_constructor/bridge_template.ini sat/bridge/bridge_constructor/constants.py sat/bridge/bridge_constructor/constructors/__init__.py sat/bridge/bridge_constructor/constructors/dbus-xml/__init__.py sat/bridge/bridge_constructor/constructors/dbus-xml/constructor.py sat/bridge/bridge_constructor/constructors/dbus-xml/dbus_xml_template.xml sat/bridge/bridge_constructor/constructors/dbus/__init__.py sat/bridge/bridge_constructor/constructors/dbus/constructor.py sat/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py sat/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py sat/bridge/bridge_constructor/constructors/embedded/__init__.py sat/bridge/bridge_constructor/constructors/embedded/constructor.py sat/bridge/bridge_constructor/constructors/embedded/embedded_frontend_template.py sat/bridge/bridge_constructor/constructors/embedded/embedded_template.py sat/bridge/bridge_constructor/constructors/mediawiki/__init__.py sat/bridge/bridge_constructor/constructors/mediawiki/constructor.py sat/bridge/bridge_constructor/constructors/mediawiki/mediawiki_template.tpl sat/bridge/bridge_constructor/constructors/pb/__init__.py sat/bridge/bridge_constructor/constructors/pb/constructor.py sat/bridge/bridge_constructor/constructors/pb/pb_core_template.py sat/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py sat/bridge/dbus_bridge.py sat/core/__init__.py sat/core/constants.py sat/core/exceptions.py sat/core/i18n.py sat/core/log.py sat/core/log_config.py sat/core/sat_main.py sat/core/xmpp.py sat/memory/__init__.py sat/memory/cache.py sat/memory/crypto.py sat/memory/disco.py sat/memory/memory.py sat/memory/params.py sat/memory/persistent.py sat/memory/sqlite.py sat/plugins/__init__.py sat/plugins/plugin_adhoc_dbus.py sat/plugins/plugin_blog_import.py sat/plugins/plugin_blog_import_dokuwiki.py sat/plugins/plugin_blog_import_dotclear.py sat/plugins/plugin_comp_file_sharing.py sat/plugins/plugin_exp_command_export.py sat/plugins/plugin_exp_events.py sat/plugins/plugin_exp_jingle_stream.py sat/plugins/plugin_exp_lang_detect.py sat/plugins/plugin_exp_parrot.py sat/plugins/plugin_exp_pubsub_hook.py sat/plugins/plugin_exp_pubsub_schema.py sat/plugins/plugin_import.py sat/plugins/plugin_merge_req_mercurial.py sat/plugins/plugin_misc_account.py sat/plugins/plugin_misc_android.py sat/plugins/plugin_misc_debug.py sat/plugins/plugin_misc_extra_pep.py sat/plugins/plugin_misc_file.py sat/plugins/plugin_misc_forums.py sat/plugins/plugin_misc_groupblog.py sat/plugins/plugin_misc_identity.py sat/plugins/plugin_misc_imap.py sat/plugins/plugin_misc_invitations.py sat/plugins/plugin_misc_ip.py sat/plugins/plugin_misc_maildir.py sat/plugins/plugin_misc_merge_requests.py sat/plugins/plugin_misc_nat-port.py sat/plugins/plugin_misc_quiz.py sat/plugins/plugin_misc_radiocol.py sat/plugins/plugin_misc_register_account.py sat/plugins/plugin_misc_room_game.py sat/plugins/plugin_misc_smtp.py sat/plugins/plugin_misc_static_blog.py sat/plugins/plugin_misc_tarot.py sat/plugins/plugin_misc_text_commands.py sat/plugins/plugin_misc_text_syntaxes.py sat/plugins/plugin_misc_tickets.py sat/plugins/plugin_misc_upload.py sat/plugins/plugin_misc_uri_finder.py sat/plugins/plugin_misc_watched.py sat/plugins/plugin_misc_welcome.py sat/plugins/plugin_misc_xmllog.py sat/plugins/plugin_sec_otr.py sat/plugins/plugin_syntax_wiki_dotclear.py sat/plugins/plugin_tickets_import.py sat/plugins/plugin_tickets_import_bugzilla.py sat/plugins/plugin_tmp_directory_subscription.py sat/plugins/plugin_xep_0020.py sat/plugins/plugin_xep_0033.py sat/plugins/plugin_xep_0045.py sat/plugins/plugin_xep_0047.py sat/plugins/plugin_xep_0048.py sat/plugins/plugin_xep_0049.py sat/plugins/plugin_xep_0050.py sat/plugins/plugin_xep_0054.py sat/plugins/plugin_xep_0055.py sat/plugins/plugin_xep_0059.py sat/plugins/plugin_xep_0060.py sat/plugins/plugin_xep_0065.py sat/plugins/plugin_xep_0070.py sat/plugins/plugin_xep_0071.py sat/plugins/plugin_xep_0077.py sat/plugins/plugin_xep_0085.py sat/plugins/plugin_xep_0092.py sat/plugins/plugin_xep_0095.py sat/plugins/plugin_xep_0096.py sat/plugins/plugin_xep_0100.py sat/plugins/plugin_xep_0115.py sat/plugins/plugin_xep_0163.py sat/plugins/plugin_xep_0166.py sat/plugins/plugin_xep_0184.py sat/plugins/plugin_xep_0203.py sat/plugins/plugin_xep_0231.py sat/plugins/plugin_xep_0234.py sat/plugins/plugin_xep_0249.py sat/plugins/plugin_xep_0260.py sat/plugins/plugin_xep_0261.py sat/plugins/plugin_xep_0264.py sat/plugins/plugin_xep_0277.py sat/plugins/plugin_xep_0280.py sat/plugins/plugin_xep_0297.py sat/plugins/plugin_xep_0300.py sat/plugins/plugin_xep_0313.py sat/plugins/plugin_xep_0329.py sat/plugins/plugin_xep_0334.py sat/plugins/plugin_xep_0363.py sat/stdui/__init__.py sat/stdui/ui_contact_list.py sat/stdui/ui_profile_manager.py sat/test/__init__.py sat/test/constants.py sat/test/helpers.py sat/test/helpers_plugins.py sat/test/test_core_xmpp.py sat/test/test_helpers_plugins.py sat/test/test_memory.py sat/test/test_memory_crypto.py sat/test/test_plugin_misc_groupblog.py sat/test/test_plugin_misc_radiocol.py sat/test/test_plugin_misc_room_game.py sat/test/test_plugin_misc_text_syntaxes.py sat/test/test_plugin_xep_0033.py sat/test/test_plugin_xep_0085.py sat/test/test_plugin_xep_0203.py sat/test/test_plugin_xep_0277.py sat/test/test_plugin_xep_0297.py sat/test/test_plugin_xep_0313.py sat/test/test_plugin_xep_0334.py sat/tools/__init__.py sat/tools/common/__init__.py sat/tools/common/ansi.py sat/tools/common/data_format.py sat/tools/common/data_objects.py sat/tools/common/dynamic_import.py sat/tools/common/regex.py sat/tools/common/template.py sat/tools/common/template_xmlui.py sat/tools/common/uri.py sat/tools/config.py sat/tools/email.py sat/tools/sat_defer.py sat/tools/stream.py sat/tools/trigger.py sat/tools/utils.py sat/tools/xml_tools.py sat_frontends/__init__.py sat_frontends/bridge/__init__.py sat_frontends/bridge/bridge_frontend.py sat_frontends/bridge/dbus_bridge.py sat_frontends/jp/__init__.py sat_frontends/jp/arg_tools.py sat_frontends/jp/base.py sat_frontends/jp/cmd_account.py sat_frontends/jp/cmd_adhoc.py sat_frontends/jp/cmd_avatar.py sat_frontends/jp/cmd_blog.py sat_frontends/jp/cmd_bookmarks.py sat_frontends/jp/cmd_debug.py sat_frontends/jp/cmd_event.py sat_frontends/jp/cmd_file.py sat_frontends/jp/cmd_forums.py sat_frontends/jp/cmd_identity.py sat_frontends/jp/cmd_info.py sat_frontends/jp/cmd_input.py sat_frontends/jp/cmd_invitation.py sat_frontends/jp/cmd_merge_request.py sat_frontends/jp/cmd_message.py sat_frontends/jp/cmd_param.py sat_frontends/jp/cmd_pipe.py sat_frontends/jp/cmd_profile.py sat_frontends/jp/cmd_pubsub.py sat_frontends/jp/cmd_roster.py sat_frontends/jp/cmd_shell.py sat_frontends/jp/cmd_ticket.py sat_frontends/jp/cmd_uri.py sat_frontends/jp/common.py sat_frontends/jp/constants.py sat_frontends/jp/jp sat_frontends/jp/output_std.py sat_frontends/jp/output_template.py sat_frontends/jp/output_xml.py sat_frontends/jp/xmlui_manager.py sat_frontends/primitivus/__init__.py sat_frontends/primitivus/chat.py sat_frontends/primitivus/config.py sat_frontends/primitivus/constants.py sat_frontends/primitivus/contact_list.py sat_frontends/primitivus/game_tarot.py sat_frontends/primitivus/keys.py sat_frontends/primitivus/notify.py sat_frontends/primitivus/primitivus sat_frontends/primitivus/profile_manager.py sat_frontends/primitivus/progress.py sat_frontends/primitivus/status.py sat_frontends/primitivus/widget.py sat_frontends/primitivus/xmlui.py sat_frontends/quick_frontend/__init__.py sat_frontends/quick_frontend/constants.py sat_frontends/quick_frontend/quick_app.py sat_frontends/quick_frontend/quick_blog.py sat_frontends/quick_frontend/quick_chat.py sat_frontends/quick_frontend/quick_contact_list.py sat_frontends/quick_frontend/quick_contact_management.py sat_frontends/quick_frontend/quick_game_tarot.py sat_frontends/quick_frontend/quick_games.py sat_frontends/quick_frontend/quick_list_manager.py sat_frontends/quick_frontend/quick_menus.py sat_frontends/quick_frontend/quick_profile_manager.py sat_frontends/quick_frontend/quick_utils.py sat_frontends/quick_frontend/quick_widgets.py sat_frontends/tools/__init__.py sat_frontends/tools/composition.py sat_frontends/tools/css_color.py sat_frontends/tools/games.py sat_frontends/tools/host_listener.py sat_frontends/tools/jid.py sat_frontends/tools/misc.py sat_frontends/tools/strings.py sat_frontends/tools/xmltools.py sat_frontends/tools/xmlui.py setup.py src/__init__.py src/bridge/__init__.py src/bridge/bridge_constructor/__init__.py src/bridge/bridge_constructor/base_constructor.py src/bridge/bridge_constructor/bridge_constructor.py src/bridge/bridge_constructor/bridge_template.ini src/bridge/bridge_constructor/constants.py src/bridge/bridge_constructor/constructors/__init__.py src/bridge/bridge_constructor/constructors/dbus-xml/__init__.py src/bridge/bridge_constructor/constructors/dbus-xml/constructor.py src/bridge/bridge_constructor/constructors/dbus-xml/dbus_xml_template.xml src/bridge/bridge_constructor/constructors/dbus/__init__.py src/bridge/bridge_constructor/constructors/dbus/constructor.py src/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py src/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py src/bridge/bridge_constructor/constructors/embedded/__init__.py src/bridge/bridge_constructor/constructors/embedded/constructor.py src/bridge/bridge_constructor/constructors/embedded/embedded_frontend_template.py src/bridge/bridge_constructor/constructors/embedded/embedded_template.py src/bridge/bridge_constructor/constructors/mediawiki/__init__.py src/bridge/bridge_constructor/constructors/mediawiki/constructor.py src/bridge/bridge_constructor/constructors/mediawiki/mediawiki_template.tpl src/bridge/bridge_constructor/constructors/pb/__init__.py src/bridge/bridge_constructor/constructors/pb/constructor.py src/bridge/bridge_constructor/constructors/pb/pb_core_template.py src/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py src/bridge/dbus_bridge.py src/core/__init__.py src/core/constants.py src/core/exceptions.py src/core/i18n.py src/core/log.py src/core/log_config.py src/core/sat_main.py src/core/xmpp.py src/memory/__init__.py src/memory/cache.py src/memory/crypto.py src/memory/disco.py src/memory/memory.py src/memory/params.py src/memory/persistent.py src/memory/sqlite.py src/plugins/__init__.py src/plugins/plugin_adhoc_dbus.py src/plugins/plugin_blog_import.py src/plugins/plugin_blog_import_dokuwiki.py src/plugins/plugin_blog_import_dotclear.py src/plugins/plugin_comp_file_sharing.py src/plugins/plugin_exp_command_export.py src/plugins/plugin_exp_events.py src/plugins/plugin_exp_jingle_stream.py src/plugins/plugin_exp_lang_detect.py src/plugins/plugin_exp_parrot.py src/plugins/plugin_exp_pubsub_hook.py src/plugins/plugin_exp_pubsub_schema.py src/plugins/plugin_import.py src/plugins/plugin_merge_req_mercurial.py src/plugins/plugin_misc_account.py src/plugins/plugin_misc_android.py src/plugins/plugin_misc_debug.py src/plugins/plugin_misc_extra_pep.py src/plugins/plugin_misc_file.py src/plugins/plugin_misc_forums.py src/plugins/plugin_misc_groupblog.py src/plugins/plugin_misc_identity.py src/plugins/plugin_misc_imap.py src/plugins/plugin_misc_invitations.py src/plugins/plugin_misc_ip.py src/plugins/plugin_misc_maildir.py src/plugins/plugin_misc_merge_requests.py src/plugins/plugin_misc_nat-port.py src/plugins/plugin_misc_quiz.py src/plugins/plugin_misc_radiocol.py src/plugins/plugin_misc_register_account.py src/plugins/plugin_misc_room_game.py src/plugins/plugin_misc_smtp.py src/plugins/plugin_misc_static_blog.py src/plugins/plugin_misc_tarot.py src/plugins/plugin_misc_text_commands.py src/plugins/plugin_misc_text_syntaxes.py src/plugins/plugin_misc_tickets.py src/plugins/plugin_misc_upload.py src/plugins/plugin_misc_uri_finder.py src/plugins/plugin_misc_watched.py src/plugins/plugin_misc_welcome.py src/plugins/plugin_misc_xmllog.py src/plugins/plugin_sec_otr.py src/plugins/plugin_syntax_wiki_dotclear.py src/plugins/plugin_tickets_import.py src/plugins/plugin_tickets_import_bugzilla.py src/plugins/plugin_tmp_directory_subscription.py src/plugins/plugin_xep_0020.py src/plugins/plugin_xep_0033.py src/plugins/plugin_xep_0045.py src/plugins/plugin_xep_0047.py src/plugins/plugin_xep_0048.py src/plugins/plugin_xep_0049.py src/plugins/plugin_xep_0050.py src/plugins/plugin_xep_0054.py src/plugins/plugin_xep_0055.py src/plugins/plugin_xep_0059.py src/plugins/plugin_xep_0060.py src/plugins/plugin_xep_0065.py src/plugins/plugin_xep_0070.py src/plugins/plugin_xep_0071.py src/plugins/plugin_xep_0077.py src/plugins/plugin_xep_0085.py src/plugins/plugin_xep_0092.py src/plugins/plugin_xep_0095.py src/plugins/plugin_xep_0096.py src/plugins/plugin_xep_0100.py src/plugins/plugin_xep_0115.py src/plugins/plugin_xep_0163.py src/plugins/plugin_xep_0166.py src/plugins/plugin_xep_0184.py src/plugins/plugin_xep_0203.py src/plugins/plugin_xep_0231.py src/plugins/plugin_xep_0234.py src/plugins/plugin_xep_0249.py src/plugins/plugin_xep_0260.py src/plugins/plugin_xep_0261.py src/plugins/plugin_xep_0264.py src/plugins/plugin_xep_0277.py src/plugins/plugin_xep_0280.py src/plugins/plugin_xep_0297.py src/plugins/plugin_xep_0300.py src/plugins/plugin_xep_0313.py src/plugins/plugin_xep_0329.py src/plugins/plugin_xep_0334.py src/plugins/plugin_xep_0363.py src/sat.sh src/stdui/__init__.py src/stdui/ui_contact_list.py src/stdui/ui_profile_manager.py src/test/__init__.py src/test/constants.py src/test/helpers.py src/test/helpers_plugins.py src/test/test_core_xmpp.py src/test/test_helpers_plugins.py src/test/test_memory.py src/test/test_memory_crypto.py src/test/test_plugin_misc_groupblog.py src/test/test_plugin_misc_radiocol.py src/test/test_plugin_misc_room_game.py src/test/test_plugin_misc_text_syntaxes.py src/test/test_plugin_xep_0033.py src/test/test_plugin_xep_0085.py src/test/test_plugin_xep_0203.py src/test/test_plugin_xep_0277.py src/test/test_plugin_xep_0297.py src/test/test_plugin_xep_0313.py src/test/test_plugin_xep_0334.py src/tools/__init__.py src/tools/common/__init__.py src/tools/common/ansi.py src/tools/common/data_format.py src/tools/common/data_objects.py src/tools/common/dynamic_import.py src/tools/common/regex.py src/tools/common/template.py src/tools/common/template_xmlui.py src/tools/common/uri.py src/tools/config.py src/tools/email.py src/tools/sat_defer.py src/tools/stream.py src/tools/trigger.py src/tools/utils.py src/tools/xml_tools.py src/twisted/plugins/sat_plugin.py twisted/plugins/sat_plugin.py |
diffstat | 456 files changed, 65711 insertions(+), 65938 deletions(-) [+] |
line wrap: on
line diff
--- a/MANIFEST.in Mon Apr 02 08:56:24 2018 +0200 +++ b/MANIFEST.in Mon Apr 02 19:44:50 2018 +0200 @@ -7,6 +7,6 @@ include src/sat.* include misc include frontends/src/jp/jp frontends/src/primitivus/primitivus -include src/bridge/bridge_constructor/mediawiki_template.tpl +include src/bridge/bridge_constructor global-exclude *.un~ prune src/bridge/bridge_constructor/generated
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/sat Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,107 @@ +#!/bin/sh + +DEBUG="" +DAEMON="" +PYTHON="python2" +TWISTD="$(which twistd)" + +kill_process() { + # $1 is the file containing the PID to kill, $2 is the process name + if [ -f $1 ]; then + PID=`cat $1` + if ps -p $PID > /dev/null; then + printf "Terminating $2... " + kill $PID + while ps -p $PID > /dev/null; do + sleep 0.2 + done + printf "OK\n" + else + echo "No running process of ID $PID... removing PID file" + rm -f $1 + fi + else + echo "$2 is probably not running (PID file doesn't exist)" + fi +} + +#We use python to parse config files +eval `"$PYTHON" << PYTHONEND +from sat.core.constants import Const as C +from sat.memory.memory import fixLocalDir +from ConfigParser import SafeConfigParser +from os.path import expanduser, join +import sys +import codecs +import locale + +sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) + +fixLocalDir() # XXX: tmp update code, will be removed in the future + +config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) +try: + config.read(C.CONFIG_FILES) +except: + print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") + print ("exit 1") + sys.exit() + +env=[] +env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) +env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) +env.append("APP_NAME='%s'" % C.APP_NAME) +env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE) +print ";".join(env) +PYTHONEND +` +APP_NAME="$APP_NAME" +PID_FILE="$PID_DIR$APP_NAME_FILE.pid" +LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" +RUNNING_MSG="$APP_NAME is running" +NOT_RUNNING_MSG="$APP_NAME is *NOT* running" + +# if there is one argument which is "stop", then we kill SaT +if [ $# -eq 1 ];then + if [ $1 = "stop" ];then + kill_process $PID_FILE "$APP_NAME" + exit 0 + elif [ $1 = "debug" ];then + echo "Launching $APP_NAME in debug mode" + DEBUG="--debug" + elif [ $1 = "fg" ];then + echo "Launching $APP_NAME in foreground mode" + DAEMON="n" + elif [ $1 = "status" ];then + if [ -f $PID_FILE ]; then + PID=`cat $PID_FILE` + ps -p$PID 2>&1 > /dev/null + if [ $? = 0 ];then + echo "$RUNNING_MSG (pid: $PID)" + exit 0 + else + echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE" + exit 2 + fi + else + echo "$NOT_RUNNING_MSG" + exit 1 + fi + else + echo "bad argument, please use one of (stop, debug, fg, status) or no argument" + exit 1 + fi +fi + +MAIN_OPTIONS="-${DAEMON}o" + +#Don't change the next lines +AUTO_OPTIONS="" +ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" + +log_dir=`dirname "$LOG_FILE"` +if [ ! -d $log_dir ] ; then + mkdir $log_dir +fi + +exec $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE
--- a/frontends/src/bridge/bridge_frontend.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -#!/usr/bin/env python2 -#-*- coding: utf-8 -*- - -# SAT communication bridge -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -class BridgeException(Exception): - """An exception which has been raised from the backend and arrived to the frontend.""" - - def __init__(self, name, message='', condition=''): - """ - - @param name (str): full exception class name (with module) - @param message (str): error message - @param condition (str) : error condition - """ - Exception.__init__(self) - self.fullname = unicode(name) - self.message = unicode(message) - self.condition = unicode(condition) if condition else '' - self.module, dummy, self.classname = unicode(self.fullname).rpartition('.') - - def __str__(self): - message = (': %s' % self.message) if self.message else '' - return self.classname + message - - def __eq__(self, other): - return self.classname == other
--- a/frontends/src/bridge/dbus_bridge.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,732 +0,0 @@ -#!/usr/bin/env python2 -#-*- coding: utf-8 -*- - -# SAT communication bridge -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.i18n import _ -from bridge_frontend import BridgeException -import dbus -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.exceptions import BridgeExceptionNoService, BridgeInitError - -from dbus.mainloop.glib import DBusGMainLoop -DBusGMainLoop(set_as_default=True) - -import ast - -const_INT_PREFIX = "org.goffi.SAT" # Interface prefix -const_ERROR_PREFIX = const_INT_PREFIX + ".error" -const_OBJ_PATH = '/org/goffi/SAT/bridge' -const_CORE_SUFFIX = ".core" -const_PLUGIN_SUFFIX = ".plugin" -const_TIMEOUT = 120 - - -def dbus_to_bridge_exception(dbus_e): - """Convert a DBusException to a BridgeException. - - @param dbus_e (DBusException) - @return: BridgeException - """ - full_name = dbus_e.get_dbus_name() - if full_name.startswith(const_ERROR_PREFIX): - name = dbus_e.get_dbus_name()[len(const_ERROR_PREFIX) + 1:] - else: - name = full_name - # XXX: dbus_e.args doesn't contain the original DBusException args, but we - # receive its serialized form in dbus_e.args[0]. From that we can rebuild - # the original arguments list thanks to ast.literal_eval (secure eval). - message = dbus_e.get_dbus_message() # similar to dbus_e.args[0] - try: - message, condition = ast.literal_eval(message) - except (SyntaxError, ValueError, TypeError): - condition = '' - return BridgeException(name, message, condition) - - -class Bridge(object): - - def bridgeConnect(self, callback, errback): - try: - self.sessions_bus = dbus.SessionBus() - self.db_object = self.sessions_bus.get_object(const_INT_PREFIX, - const_OBJ_PATH) - self.db_core_iface = dbus.Interface(self.db_object, - dbus_interface=const_INT_PREFIX + const_CORE_SUFFIX) - self.db_plugin_iface = dbus.Interface(self.db_object, - dbus_interface=const_INT_PREFIX + const_PLUGIN_SUFFIX) - except dbus.exceptions.DBusException, e: - if e._dbus_error_name in ('org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Spawn.ExecFailed'): - errback(BridgeExceptionNoService()) - elif e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': - log.error(_(u"D-Bus is not launched, please see README to see instructions on how to launch it")) - errback(BridgeInitError) - else: - errback(e) - callback() - #props = self.db_core_iface.getProperties() - - def register_signal(self, functionName, handler, iface="core"): - if iface == "core": - self.db_core_iface.connect_to_signal(functionName, handler) - elif iface == "plugin": - self.db_plugin_iface.connect_to_signal(functionName, handler) - else: - log.error(_('Unknown interface')) - - def __getattribute__(self, name): - """ usual __getattribute__ if the method exists, else try to find a plugin method """ - try: - return object.__getattribute__(self, name) - except AttributeError: - # The attribute is not found, we try the plugin proxy to find the requested method - - def getPluginMethod(*args, **kwargs): - # We first check if we have an async call. We detect this in two ways: - # - if we have the 'callback' and 'errback' keyword arguments - # - or if the last two arguments are callable - - async = False - args = list(args) - - if kwargs: - if 'callback' in kwargs: - async = True - _callback = kwargs.pop('callback') - _errback = kwargs.pop('errback', lambda failure: log.error(unicode(failure))) - try: - args.append(kwargs.pop('profile')) - except KeyError: - try: - args.append(kwargs.pop('profile_key')) - except KeyError: - pass - # at this point, kwargs should be empty - if kwargs: - log.warnings(u"unexpected keyword arguments, they will be ignored: {}".format(kwargs)) - elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): - async = True - _errback = args.pop() - _callback = args.pop() - - method = getattr(self.db_plugin_iface, name) - - if async: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = _callback - kwargs['error_handler'] = lambda err: _errback(dbus_to_bridge_exception(err)) - - return method(*args, **kwargs) - - return getPluginMethod - - def actionsGet(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.actionsGet(profile_key, **kwargs) - - def addContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.addContact(entity_jid, profile_key, **kwargs) - - def asyncDeleteProfile(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.asyncDeleteProfile(profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def asyncGetParamA(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return unicode(self.db_core_iface.asyncGetParamA(name, category, attribute, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def asyncGetParamsValuesFromCategory(self, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.asyncGetParamsValuesFromCategory(category, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.connect(profile_key, password, options, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.delContact(entity_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def discoFindByFeatures(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, profile_key=u"@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.discoFindByFeatures(namespaces, identities, bare_jid, service, roster, own_jid, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def discoInfos(self, entity_jid, node=u'', use_cache=True, profile_key=u"@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.discoInfos(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def discoItems(self, entity_jid, node=u'', use_cache=True, profile_key=u"@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.discoItems(entity_jid, node, use_cache, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.disconnect(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def getConfig(self, section, name, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.getConfig(section, name, **kwargs)) - - def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.getContacts(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def getContactsFromGroup(self, group, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getContactsFromGroup(group, profile_key, **kwargs) - - def getEntitiesData(self, jids, keys, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getEntitiesData(jids, keys, profile, **kwargs) - - def getEntityData(self, jid, keys, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getEntityData(jid, keys, profile, **kwargs) - - def getFeatures(self, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.getFeatures(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def getMainResource(self, contact_jid, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.getMainResource(contact_jid, profile_key, **kwargs)) - - def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key, **kwargs)) - - def getParamsCategories(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getParamsCategories(**kwargs) - - def getParamsUI(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return unicode(self.db_core_iface.getParamsUI(security_limit, app, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)) - - def getPresenceStatuses(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getPresenceStatuses(profile_key, **kwargs) - - def getReady(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.getReady(timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def getVersion(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.getVersion(**kwargs)) - - def getWaitingSub(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.getWaitingSub(profile_key, **kwargs) - - def historyGet(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.historyGet(from_jid, to_jid, limit, between, filters, profile, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def isConnected(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.isConnected(profile_key, **kwargs) - - def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.launchAction(callback_id, data, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def loadParamsTemplate(self, filename, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.loadParamsTemplate(filename, **kwargs) - - def menuHelpGet(self, menu_id, language, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.menuHelpGet(menu_id, language, **kwargs)) - - def menuLaunch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.menuLaunch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def menusGet(self, language, security_limit, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.menusGet(language, security_limit, **kwargs) - - def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.messageSend(to_jid, message, subject, mess_type, extra, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def namespacesGet(self, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.namespacesGet(**kwargs) - - def paramsRegisterApp(self, xml, security_limit=-1, app='', callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.paramsRegisterApp(xml, security_limit, app, **kwargs) - - def profileCreate(self, profile, password='', component='', callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.profileCreate(profile, password, component, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profileIsSessionStarted(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profileIsSessionStarted(profile_key, **kwargs) - - def profileNameGet(self, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return unicode(self.db_core_iface.profileNameGet(profile_key, **kwargs)) - - def profileSetDefault(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profileSetDefault(profile, **kwargs) - - def profileStartSession(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.profileStartSession(password, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def profilesListGet(self, clients=True, components=False, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.profilesListGet(clients, components, **kwargs) - - def progressGet(self, id, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progressGet(id, profile, **kwargs) - - def progressGetAll(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progressGetAll(profile, **kwargs) - - def progressGetAllMetadata(self, profile, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.progressGetAllMetadata(profile, **kwargs) - - def saveParamsTemplate(self, filename, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.saveParamsTemplate(filename, **kwargs) - - def sessionInfosGet(self, profile_key, callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.sessionInfosGet(profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler) - - def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.setParam(name, value, category, security_limit, profile_key, **kwargs) - - def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.setPresence(to_jid, show, statuses, profile_key, **kwargs) - - def subscription(self, sub_type, entity, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - kwargs={} - if callback is not None: - kwargs['timeout'] = const_TIMEOUT - kwargs['reply_handler'] = callback - kwargs['error_handler'] = error_handler - return self.db_core_iface.subscription(sub_type, entity, profile_key, **kwargs) - - def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): - if callback is None: - error_handler = None - else: - if errback is None: - errback = log.error - error_handler = lambda err:errback(dbus_to_bridge_exception(err)) - return self.db_core_iface.updateContact(entity_jid, name, groups, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
--- a/frontends/src/jp/arg_tools.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.i18n import _ -from sat.core import exceptions - - -def escape(arg, smart=True): - """format arg with quotes - - @param smart(bool): if True, only escape if needed - """ - if smart and not ' ' in arg and not '"' in arg: - return arg - return u'"' + arg.replace(u'"',u'\\"') + u'"' - - -def get_cmd_choices(cmd=None, parser=None): - try: - choices = parser._subparsers._group_actions[0].choices - return choices[cmd] if cmd is not None else choices - except (KeyError, AttributeError): - raise exceptions.NotFound - - -def get_use_args(host, args, use, verbose=False, parser=None): - """format args for argparse parser with values prefilled - - @param host(JP): jp instance - @param args(list(str)): arguments to use - @param use(dict[str, str]): arguments to fill if found in parser - @param verbose(bool): if True a message will be displayed when argument is used or not - @param parser(argparse.ArgumentParser): parser to use - @return (tuple[list[str],list[str]]): 2 args lists: - - parser args, i.e. given args corresponding to parsers - - use args, i.e. generated args from use - """ - # FIXME: positional args are not handled correclty - # if there is more that one, the position is not corrected - if parser is None: - parser = host.parser - - # we check not optional args to see if there - # is a corresonding parser - # else USE args would not work correctly (only for current parser) - parser_args = [] - for arg in args: - if arg.startswith('-'): - break - try: - parser = get_cmd_choices(arg, parser) - except exceptions.NotFound: - break - parser_args.append(arg) - - # post_args are remaning given args, - # without the ones corresponding to parsers - post_args = args[len(parser_args):] - - opt_args = [] - pos_args = [] - actions = {a.dest: a for a in parser._actions} - for arg, value in use.iteritems(): - try: - if arg == u'item' and not u'item' in actions: - # small hack when --item is appended to a --items list - arg = u'items' - action = actions[arg] - except KeyError: - if verbose: - host.disp(_(u'ignoring {name}={value}, not corresponding to any argument (in USE)').format( - name=arg, - value=escape(value))) - else: - if verbose: - host.disp(_(u'arg {name}={value} (in USE)').format(name=arg, value=escape(value))) - if not action.option_strings: - pos_args.append(value) - else: - opt_args.append(action.option_strings[0]) - opt_args.append(value) - return parser_args, opt_args + pos_args + post_args
--- a/frontends/src/jp/base.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1084 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat.core.i18n import _ - -### logging ### -import logging as log -log.basicConfig(level=log.DEBUG, - format='%(message)s') -### - -import sys -import locale -import os.path -import argparse -from glob import iglob -from importlib import import_module -from sat_frontends.tools.jid import JID -from sat.tools import config -from sat.tools.common import dynamic_import -from sat.tools.common import uri -from sat.core import exceptions -import sat_frontends.jp -from sat_frontends.jp.constants import Const as C -from sat_frontends.tools import misc -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -import shlex -from collections import OrderedDict - -## bridge handling -# we get bridge name from conf and initialise the right class accordingly -main_config = config.parseMainConf() -bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus') - - -# TODO: move loops handling in a separated module -if 'dbus' in bridge_name: - from gi.repository import GLib - - - class JPLoop(object): - - def __init__(self): - self.loop = GLib.MainLoop() - - def run(self): - self.loop.run() - - def quit(self): - self.loop.quit() - - def call_later(self, delay, callback, *args): - """call a callback repeatedly - - @param delay(int): delay between calls in ms - @param callback(callable): method to call - if the callback return True, the call will continue - else the calls will stop - @param *args: args of the callbac - """ - GLib.timeout_add(delay, callback, *args) - -else: - print u"can't start jp: only D-Bus bridge is currently handled" - sys.exit(C.EXIT_ERROR) - # FIXME: twisted loop can be used when jp can handle fully async bridges - # from twisted.internet import reactor - - # class JPLoop(object): - - # def run(self): - # reactor.run() - - # def quit(self): - # reactor.stop() - - # def _timeout_cb(self, args, callback, delay): - # ret = callback(*args) - # if ret: - # reactor.callLater(delay, self._timeout_cb, args, callback, delay) - - # def call_later(self, delay, callback, *args): - # delay = float(delay) / 1000 - # reactor.callLater(delay, self._timeout_cb, args, callback, delay) - -if bridge_name == "embedded": - from sat.core import sat_main - sat = sat_main.SAT() - -if sys.version_info < (2, 7, 3): - # XXX: shlex.split only handle unicode since python 2.7.3 - # this is a workaround for older versions - old_split = shlex.split - new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)] - if isinstance(s, unicode) else old_split(s, *a, **kw)) - shlex.split = new_split - -try: - import progressbar -except ImportError: - msg = (_(u'ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar\n') + - _(u'Progress bar deactivated\n--\n')) - print >>sys.stderr,msg.encode('utf-8') - progressbar=None - -#consts -PROG_NAME = u"jp" -DESCRIPTION = """This software is a command line tool for XMPP. -Get the latest version at """ + C.APP_URL - -COPYLEFT = u"""Copyright (C) 2009-2018 Jérôme Poisson, Adrien Cossa -This program comes with ABSOLUTELY NO WARRANTY; -This is free software, and you are welcome to redistribute it under certain conditions. -""" - -PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms - - -def unicode_decoder(arg): - # Needed to have unicode strings from arguments - return arg.decode(locale.getpreferredencoding()) - - -class Jp(object): - """ - This class can be use to establish a connection with the - bridge. Moreover, it should manage a main loop. - - To use it, you mainly have to redefine the method run to perform - specify what kind of operation you want to perform. - - """ - def __init__(self): - """ - - @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting, - or if you want the user to stop by himself - @attribute progress_success(callable): method to call when progress just started - by default display a message - @attribute progress_success(callable): method to call when progress is successfully finished - by default display a message - @attribute progress_failure(callable): method to call when progress failed - by default display a message - """ - # FIXME: need_loop should be removed, everything must be async in bridge so - # loop will always be needed - bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge') - if bridge_module is None: - log.error(u"Can't import {} bridge".format(bridge_name)) - sys.exit(1) - - self.bridge = bridge_module.Bridge() - self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb) - - def _bridgeCb(self): - self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, - description=DESCRIPTION) - self._make_parents() - self.add_parser_options() - self.subparsers = self.parser.add_subparsers(title=_(u'Available commands'), dest='subparser_name') - self._auto_loop = False # when loop is used for internal reasons - self._need_loop = False - - # progress attributes - self._progress_id = None # TODO: manage several progress ids - self.quit_on_progress_end = True - - # outputs - self._outputs = {} - for type_ in C.OUTPUT_TYPES: - self._outputs[type_] = OrderedDict() - self.default_output = {} - - def _bridgeEb(self, failure): - if isinstance(failure, exceptions.BridgeExceptionNoService): - print(_(u"Can't connect to SàT backend, are you sure it's launched ?")) - elif isinstance(failure, exceptions.BridgeInitError): - print(_(u"Can't init bridge")) - else: - print(_(u"Error while initialising bridge: {}".format(failure))) - sys.exit(C.EXIT_BRIDGE_ERROR) - - @property - def version(self): - return self.bridge.getVersion() - - @property - def progress_id(self): - return self._progress_id - - @progress_id.setter - def progress_id(self, value): - self._progress_id = value - self.replayCache('progress_ids_cache') - - @property - def watch_progress(self): - try: - self.pbar - except AttributeError: - return False - else: - return True - - @watch_progress.setter - def watch_progress(self, watch_progress): - if watch_progress: - self.pbar = None - - @property - def verbosity(self): - try: - return self.args.verbose - except AttributeError: - return 0 - - def replayCache(self, cache_attribute): - """Replay cached signals - - @param cache_attribute(str): name of the attribute containing the cache - if the attribute doesn't exist, there is no cache and the call is ignored - else the cache must be a list of tuples containing the replay callback as first item, - then the arguments to use - """ - try: - cache = getattr(self, cache_attribute) - except AttributeError: - pass - else: - for cache_data in cache: - cache_data[0](*cache_data[1:]) - - def disp(self, msg, verbosity=0, error=False, no_lf=False): - """Print a message to user - - @param msg(unicode): message to print - @param verbosity(int): minimal verbosity to display the message - @param error(bool): if True, print to stderr instead of stdout - @param no_lf(bool): if True, do not emit line feed at the end of line - """ - if self.verbosity >= verbosity: - if error: - if no_lf: - print >>sys.stderr,msg.encode('utf-8'), - else: - print >>sys.stderr,msg.encode('utf-8') - else: - if no_lf: - print msg.encode('utf-8'), - else: - print msg.encode('utf-8') - - def output(self, type_, name, extra_outputs, data): - if name in extra_outputs: - extra_outputs[name](data) - else: - self._outputs[type_][name]['callback'](data) - - def addOnQuitCallback(self, callback, *args, **kwargs): - """Add a callback which will be called on quit command - - @param callback(callback): method to call - """ - try: - callbacks_list = self._onQuitCallbacks - except AttributeError: - callbacks_list = self._onQuitCallbacks = [] - finally: - callbacks_list.append((callback, args, kwargs)) - - def getOutputChoices(self, output_type): - """Return valid output filters for output_type - - @param output_type: True for default, - else can be any registered type - """ - return self._outputs[output_type].keys() - - def _make_parents(self): - self.parents = {} - - # we have a special case here as the start-session option is present only if connection is not needed, - # so we create two similar parents, one with the option, the other one without it - for parent_name in ('profile', 'profile_session'): - parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False) - parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)")) - parent.add_argument("--pwd", action="store", type=unicode_decoder, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary")) - - profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session'] - - connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else") - profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) - - profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group() - profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help) - profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting")) - - progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False) - if progressbar: - progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar")) - - verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False) - verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)")) - - draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False) - draft_group = draft_parent.add_argument_group(_('draft handling')) - draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft")) - draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve")) - - - def make_pubsub_group(self, flags, defaults): - """generate pubsub options according to flags - - @param flags(iterable[unicode]): see [CommandBase.__init__] - @param defaults(dict[unicode, unicode]): help text for default value - key can be "service" or "node" - value will be set in " (DEFAULT: {value})", or can be None to remove DEFAULT - @return (ArgumentParser): parser to add - """ - flags = misc.FlagsHandler(flags) - parent = argparse.ArgumentParser(add_help=False) - pubsub_group = parent.add_argument_group('pubsub') - pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder, - help=_(u"Pubsub URL (xmpp or http)")) - - service_help = _(u"JID of the PubSub service") - if not flags.service: - default = defaults.pop(u'service', _(u'PEP service')) - if default is not None: - service_help += _(u" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'', - help=service_help) - - node_help = _(u"node to request") - if not flags.node: - default = defaults.pop(u'node', _(u'standard node')) - if default is not None: - node_help += _(u" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help) - - if flags.single_item: - item_help = (u"item to retrieve") - if not flags.item: - default = defaults.pop(u'item', _(u'last item')) - if default is not None: - item_help += _(u" (DEFAULT: {default})".format(default=default)) - pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=item_help) - pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item')) - elif flags.multi_items: - # mutiple items - pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)")) - if not flags.no_max: - pubsub_group.add_argument("-m", "--max", type=int, default=10, - help=_(u"maximum number of items to get ({no_limit} to get all items)".format(no_limit=C.NO_LIMIT))) - - if flags: - raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags))) - if defaults: - raise exceptions.InternalError('unused defaults: {defaults}'.format(defaults=defaults)) - - return parent - - def add_parser_options(self): - self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT})) - - def register_output(self, type_, name, callback, description="", default=False): - if type_ not in C.OUTPUT_TYPES: - log.error(u"Invalid output type {}".format(type_)) - return - self._outputs[type_][name] = {'callback': callback, - 'description': description - } - if default: - if type_ in self.default_output: - self.disp(_(u'there is already a default output for {}, ignoring new one').format(type_)) - else: - self.default_output[type_] = name - - - def parse_output_options(self): - options = self.command.args.output_opts - options_dict = {} - for option in options: - try: - key, value = option.split(u'=', 1) - except ValueError: - key, value = option, None - options_dict[key.strip()] = value.strip() if value is not None else None - return options_dict - - def check_output_options(self, accepted_set, options): - if not accepted_set.issuperset(options): - self.disp(u"The following output options are invalid: {invalid_options}".format( - invalid_options = u', '.join(set(options).difference(accepted_set))), - error=True) - self.quit(C.EXIT_BAD_ARG) - - def import_plugins(self): - """Automaticaly import commands and outputs in jp - - looks from modules names cmd_*.py in jp path and import them - """ - path = os.path.dirname(sat_frontends.jp.__file__) - # XXX: outputs must be imported before commands as they are used for arguments - for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')): - modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern)))) - for module_name in modules: - module_path = "sat_frontends.jp." + module_name - try: - module = import_module(module_path) - self.import_plugin_module(module, type_) - except ImportError as e: - self.disp(_(u"Can't import {module_path} plugin, ignoring it: {msg}".format( - module_path = module_path, - msg = e)), error=True) - except exceptions.CancelError: - continue - except exceptions.MissingModule as e: - self.disp(_(u"Missing module for plugin {name}: {missing}".format( - name = module_path, - missing = e)), error=True) - - - def import_plugin_module(self, module, type_): - """add commands or outpus from a module to jp - - @param module: module containing commands or outputs - @param type_(str): one of C_PLUGIN_* - """ - try: - class_names = getattr(module, '__{}__'.format(type_)) - except AttributeError: - log.disp(_(u"Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True) - raise ImportError - else: - for class_name in class_names: - cls = getattr(module, class_name) - cls(self) - - def get_xmpp_uri_from_http(self, http_url): - """parse HTML page at http(s) URL, and looks for xmpp: uri""" - if http_url.startswith('https'): - scheme = u'https' - elif http_url.startswith('http'): - scheme = u'http' - else: - raise exceptions.InternalError(u'An HTTP scheme is expected in this method') - self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1) - # HTTP URL, we try to find xmpp: links - try: - from lxml import etree - except ImportError: - self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True) - self.host.quit(1) - import urllib2 - parser = etree.HTMLParser() - try: - root = etree.parse(urllib2.urlopen(http_url), parser) - except etree.XMLSyntaxError as e: - self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e)) - links = [] - else: - links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]") - if not links: - self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True) - self.host.quit(1) - xmpp_uri = links[0].get('href') - return xmpp_uri - - def parse_pubsub_args(self): - if self.args.pubsub_url is not None: - url = self.args.pubsub_url - - if url.startswith('http'): - # http(s) URL, we try to retrieve xmpp one from there - url = self.get_xmpp_uri_from_http(url) - - try: - uri_data = uri.parseXMPPUri(url) - except ValueError: - self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url)) - else: - if uri_data[u'type'] == 'pubsub': - # URL is alright, we only set data not already set by other options - if not self.args.service: - self.args.service = uri_data[u'path'] - if not self.args.node: - self.args.node = uri_data[u'node'] - uri_item = uri_data.get(u'item') - if uri_item: - # there is an item in URI - # we use it only if item is not already set - # and item_last is not used either - try: - item = self.args.item - except AttributeError: - if not self.args.items: - self.args.items = [uri_item] - else: - if not item: - try: - item_last = self.args.item_last - except AttributeError: - item_last = False - if not item_last: - self.args.item = uri_item - else: - self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url)) - flags = self.args._cmd._pubsub_flags - # we check required arguments here instead of using add_arguments' required option - # because the required argument can be set in URL - if C.SERVICE in flags and not self.args.service: - self.parser.error(_(u"argument -s/--service is required")) - if C.NODE in flags and not self.args.node: - self.parser.error(_(u"argument -n/--node is required")) - if C.ITEM in flags and not self.args.item: - self.parser.error(_(u"argument -i/--item is required")) - - # FIXME: mutually groups can't be nested in a group and don't support title - # so we check conflict here. This may be fixed in Python 3, to be checked - try: - if self.args.item and self.args.item_last: - self.parser.error(_(u"--item and --item-last can't be used at the same time")) - except AttributeError: - pass - - def run(self, args=None, namespace=None): - self.args = self.parser.parse_args(args, namespace=None) - if self.args._cmd._use_pubsub: - self.parse_pubsub_args() - try: - self.args._cmd.run() - if self._need_loop or self._auto_loop: - self._start_loop() - except KeyboardInterrupt: - log.info(_("User interruption: good bye")) - - def _start_loop(self): - self.loop = JPLoop() - self.loop.run() - - def stop_loop(self): - try: - self.loop.quit() - except AttributeError: - pass - - def confirmOrQuit(self, message, cancel_message=_(u"action cancelled by user")): - """Request user to confirm action, and quit if he doesn't""" - - res = raw_input("{} (y/N)? ".format(message)) - if res not in ("y", "Y"): - self.disp(cancel_message) - self.quit(C.EXIT_USER_CANCELLED) - - def quitFromSignal(self, errcode=0): - """Same as self.quit, but from a signal handler - - /!\: return must be used after calling this method ! - """ - assert self._need_loop - # XXX: python-dbus will show a traceback if we exit in a signal handler - # so we use this little timeout trick to avoid it - self.loop.call_later(0, self.quit, errcode) - - def quit(self, errcode=0): - # first the onQuitCallbacks - try: - callbacks_list = self._onQuitCallbacks - except AttributeError: - pass - else: - for callback, args, kwargs in callbacks_list: - callback(*args, **kwargs) - - self.stop_loop() - sys.exit(errcode) - - def check_jids(self, jids): - """Check jids validity, transform roster name to corresponding jids - - @param profile: profile name - @param jids: list of jids - @return: List of jids - - """ - names2jid = {} - nodes2jid = {} - - for contact in self.bridge.getContacts(self.profile): - jid_s, attr, groups = contact - _jid = JID(jid_s) - try: - names2jid[attr["name"].lower()] = jid_s - except KeyError: - pass - - if _jid.node: - nodes2jid[_jid.node.lower()] = jid_s - - def expand_jid(jid): - _jid = jid.lower() - if _jid in names2jid: - expanded = names2jid[_jid] - elif _jid in nodes2jid: - expanded = nodes2jid[_jid] - else: - expanded = jid - return expanded.decode('utf-8') - - def check(jid): - if not jid.is_valid: - log.error (_("%s is not a valid JID !"), jid) - self.quit(1) - - dest_jids=[] - try: - for i in range(len(jids)): - dest_jids.append(expand_jid(jids[i])) - check(dest_jids[i]) - except AttributeError: - pass - - return dest_jids - - def connect_profile(self, callback): - """ Check if the profile is connected and do it if requested - - @param callback: method to call when profile is connected - @exit: - 1 when profile is not connected and --connect is not set - - 1 when the profile doesn't exists - - 1 when there is a connection error - """ - # FIXME: need better exit codes - - def cant_connect(failure): - log.error(_(u"Can't connect profile: {reason}").format(reason=failure)) - self.quit(1) - - def cant_start_session(failure): - log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure)) - self.quit(1) - - self.profile = self.bridge.profileNameGet(self.args.profile) - - if not self.profile: - log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile)) - self.quit(1) - - try: - start_session = self.args.start_session - except AttributeError: - pass - else: - if start_session: - self.bridge.profileStartSession(self.args.pwd, self.profile, lambda dummy: callback(), cant_start_session) - self._auto_loop = True - return - elif not self.bridge.profileIsSessionStarted(self.profile): - if not self.args.connect: - 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)) - self.quit(1) - else: - callback() - return - - - if not hasattr(self.args, 'connect'): - # a profile can be present without connect option (e.g. on profile creation/deletion) - return - elif self.args.connect is True: # if connection is asked, we connect the profile - self.bridge.connect(self.profile, self.args.pwd, {}, lambda dummy: callback(), cant_connect) - self._auto_loop = True - return - else: - if not self.bridge.isConnected(self.profile): - log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile)) - self.quit(1) - - callback() - - def get_full_jid(self, param_jid): - """Return the full jid if possible (add main resource when find a bare jid)""" - _jid = JID(param_jid) - if not _jid.resource: - #if the resource is not given, we try to add the main resource - main_resource = self.bridge.getMainResource(param_jid, self.profile) - if main_resource: - return "%s/%s" % (_jid.bare, main_resource) - return param_jid - - -class CommandBase(object): - - def __init__(self, host, name, use_profile=True, use_output=False, extra_outputs=None, - need_connect=None, help=None, **kwargs): - """Initialise CommandBase - - @param host: Jp instance - @param name(unicode): name of the new command - @param use_profile(bool): if True, add profile selection/connection commands - @param use_output(bool, unicode): if not False, add --output option - @param extra_outputs(dict): list of command specific outputs: - key is output name ("default" to use as main output) - value is a callable which will format the output (data will be used as only argument) - if a key already exists with normal outputs, the extra one will be used - @param need_connect(bool, None): True if profile connection is needed - False else (profile session must still be started) - None to set auto value (i.e. True if use_profile is set) - Can't be set if use_profile is False - @param help(unicode): help message to display - @param **kwargs: args passed to ArgumentParser - use_* are handled directly, they can be: - - use_progress(bool): if True, add progress bar activation option - progress* signals will be handled - - use_verbose(bool): if True, add verbosity option - - use_pubsub(bool): if True, add pubsub options - mandatory arguments are controlled by pubsub_req - - use_draft(bool): if True, add draft handling options - ** other arguments ** - - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be: - C.SERVICE: service is required - C.NODE: node is required - C.SINGLE_ITEM: only one item is allowed - @attribute need_loop(bool): to set by commands when loop is needed - """ - self.need_loop = False # to be set by commands when loop is needed - try: # If we have subcommands, host is a CommandBase and we need to use host.host - self.host = host.host - except AttributeError: - self.host = host - - # --profile option - parents = kwargs.setdefault('parents', set()) - if use_profile: - #self.host.parents['profile'] is an ArgumentParser with profile connection arguments - if need_connect is None: - need_connect = True - parents.add(self.host.parents['profile' if need_connect else 'profile_session']) - else: - assert need_connect is None - self.need_connect = need_connect - # from this point, self.need_connect is None if connection is not needed at all - # False if session starting is needed, and True if full connection is needed - - # --output option - if use_output: - if extra_outputs is None: - extra_outputs = {} - self.extra_outputs = extra_outputs - if use_output == True: - use_output = C.OUTPUT_TEXT - assert use_output in C.OUTPUT_TYPES - self._output_type = use_output - output_parent = argparse.ArgumentParser(add_help=False) - choices = set(self.host.getOutputChoices(use_output)) - choices.update(extra_outputs) - if not choices: - raise exceptions.InternalError("No choice found for {} output type".format(use_output)) - try: - default = self.host.default_output[use_output] - except KeyError: - if u'default' in choices: - default = u'default' - elif u'simple' in choices: - default = u'simple' - else: - default = list(choices)[0] - output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_(u"select output format (default: {})".format(default))) - output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option")) - parents.add(output_parent) - else: - assert extra_outputs is None - - self._use_pubsub = kwargs.pop('use_pubsub', False) - if self._use_pubsub: - flags = kwargs.pop('pubsub_flags', []) - defaults = kwargs.pop('pubsub_defaults', {}) - parents.add(self.host.make_pubsub_group(flags, defaults)) - self._pubsub_flags = flags - - # other common options - use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')} - for param, do_use in use_opts.iteritems(): - opt=param[4:] # if param is use_verbose, opt is verbose - if opt not in self.host.parents: - raise exceptions.InternalError(u"Unknown parent option {}".format(opt)) - del kwargs[param] - if do_use: - parents.add(self.host.parents[opt]) - - self.parser = host.subparsers.add_parser(name, help=help, **kwargs) - if hasattr(self, "subcommands"): - self.subparsers = self.parser.add_subparsers() - else: - self.parser.set_defaults(_cmd=self) - self.add_parser_options() - - @property - def args(self): - return self.host.args - - @property - def profile(self): - return self.host.profile - - @property - def verbosity(self): - return self.host.verbosity - - @property - def progress_id(self): - return self.host.progress_id - - @progress_id.setter - def progress_id(self, value): - self.host.progress_id = value - - def progressStartedHandler(self, uid, metadata, profile): - if profile != self.profile: - return - if self.progress_id is None: - # the progress started message can be received before the id - # so we keep progressStarted signals in cache to replay they - # when the progress_id is received - cache_data = (self.progressStartedHandler, uid, metadata, profile) - try: - self.host.progress_ids_cache.append(cache_data) - except AttributeError: - self.host.progress_ids_cache = [cache_data] - else: - if self.host.watch_progress and uid == self.progress_id: - self.onProgressStarted(metadata) - self.host.loop.call_later(PROGRESS_DELAY, self.progressUpdate) - - def progressFinishedHandler(self, uid, metadata, profile): - if profile != self.profile: - return - if uid == self.progress_id: - try: - self.host.pbar.finish() - except AttributeError: - pass - self.onProgressFinished(metadata) - if self.host.quit_on_progress_end: - self.host.quitFromSignal() - - def progressErrorHandler(self, uid, message, profile): - if profile != self.profile: - return - if uid == self.progress_id: - if self.args.progress: - self.disp('') # progress is not finished, so we skip a line - if self.host.quit_on_progress_end: - self.onProgressError(message) - self.host.quitFromSignal(1) - - def progressUpdate(self): - """This method is continualy called to update the progress bar""" - data = self.host.bridge.progressGet(self.progress_id, self.profile) - if data: - try: - size = data['size'] - except KeyError: - self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True) - return False - if self.host.pbar is None: - #first answer, we must construct the bar - self.host.pbar = progressbar.ProgressBar(max_value=int(size), - widgets=[_(u"Progress: "),progressbar.Percentage(), - " ", - progressbar.Bar(), - " ", - progressbar.FileTransferSpeed(), - " ", - progressbar.ETA()]) - self.host.pbar.start() - - self.host.pbar.update(int(data['position'])) - - elif self.host.pbar is not None: - return False - - self.onProgressUpdate(data) - - return True - - def onProgressStarted(self, metadata): - """Called when progress has just started - - can be overidden by a command - @param metadata(dict): metadata as sent by bridge.progressStarted - """ - self.disp(_(u"Operation started"), 2) - - def onProgressUpdate(self, metadata): - """Method called on each progress updata - - can be overidden by a command to handle progress metadata - @para metadata(dict): metadata as returned by bridge.progressGet - """ - pass - - def onProgressFinished(self, metadata): - """Called when progress has just finished - - can be overidden by a command - @param metadata(dict): metadata as sent by bridge.progressFinished - """ - self.disp(_(u"Operation successfully finished"), 2) - - def onProgressError(self, error_msg): - """Called when a progress failed - - @param error_msg(unicode): error message as sent by bridge.progressError - """ - self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True) - - def disp(self, msg, verbosity=0, error=False, no_lf=False): - return self.host.disp(msg, verbosity, error, no_lf) - - def output(self, data): - try: - output_type = self._output_type - except AttributeError: - raise exceptions.InternalError(_(u'trying to use output when use_output has not been set')) - return self.host.output(output_type, self.args.output, self.extra_outputs, data) - - def exitCb(self, msg=None): - """generic callback for success - - optionally print a message, and quit - msg(None, unicode): if not None, print this message - """ - if msg is not None: - self.disp(msg) - self.host.quit(C.EXIT_OK) - - def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR): - """generic callback for errbacks - - display failure_ then quit with generic error - @param failure_: arguments returned by errback - @param msg(unicode, None): message template - use {} if you want to display failure message - @param exit_code(int): shell exit code - """ - if msg is None: - msg = _(u"error: {}") - self.disp(msg.format(failure_), error=True) - self.host.quit(exit_code) - - def add_parser_options(self): - try: - subcommands = self.subcommands - except AttributeError: - # We don't have subcommands, the class need to implements add_parser_options - raise NotImplementedError - - # now we add subcommands to ourself - for cls in subcommands: - cls(self) - - def run(self): - """this method is called when a command is actually run - - It set stuff like progression callbacks and profile connection - You should not overide this method: you should call self.start instead - """ - # we keep a reference to run command, it may be useful e.g. for outputs - self.host.command = self - # host._need_loop is set here from our current value and not before - # as the need_loop decision must be taken only by then running command - self.host._need_loop = self.need_loop - - try: - show_progress = self.args.progress - except AttributeError: - # the command doesn't use progress bar - pass - else: - if show_progress: - self.host.watch_progress = True - # we need to register the following signal even if we don't display the progress bar - self.host.bridge.register_signal("progressStarted", self.progressStartedHandler) - self.host.bridge.register_signal("progressFinished", self.progressFinishedHandler) - self.host.bridge.register_signal("progressError", self.progressErrorHandler) - - if self.need_connect is not None: - self.host.connect_profile(self.connected) - else: - self.start() - - def connected(self): - """this method is called when profile is connected (or session is started) - - this method is only called when use_profile is True - most of time you should override self.start instead of this method, but if loop - if not always needed depending on your arguments, you may override this method, - but don't forget to call the parent one (i.e. this one) after self.need_loop is set - """ - if not self.need_loop: - self.host.stop_loop() - self.start() - - def start(self): - """This is the starting point of the command, this method should be overriden - - at this point, profile are connected if needed - """ - pass - - -class CommandAnswering(CommandBase): - """Specialised commands which answer to specific actions - - to manage action_types answer, - """ - action_callbacks = {} # XXX: set managed action types in a dict here: - # key is the action_type, value is the callable - # which will manage the answer. profile filtering is - # already managed when callback is called - - def __init__(self, *args, **kwargs): - super(CommandAnswering, self).__init__(*args, **kwargs) - self.need_loop = True - - def onActionNew(self, action_data, action_id, security_limit, profile): - if profile != self.profile: - return - try: - action_type = action_data['meta_type'] - except KeyError: - try: - xml_ui = action_data["xmlui"] - except KeyError: - pass - else: - self.onXMLUI(xml_ui) - else: - try: - callback = self.action_callbacks[action_type] - except KeyError: - pass - else: - callback(action_data, action_id, security_limit, profile) - - def onXMLUI(self, xml_ui): - """Display a dialog received from the backend. - - @param xml_ui (unicode): dialog XML representation - """ - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the future - # TODO: XMLUI module - ui = ET.fromstring(xml_ui.encode('utf-8')) - dialog = ui.find("dialog") - if dialog is not None: - self.disp(dialog.findtext("message"), error=dialog.get("level") == "error") - - def connected(self): - """Auto reply to confirmations requests""" - self.need_loop = True - super(CommandAnswering, self).connected() - self.host.bridge.register_signal("actionNew", self.onActionNew) - actions = self.host.bridge.actionsGet(self.profile) - for action_data, action_id, security_limit in actions: - self.onActionNew(action_data, action_id, security_limit, self.profile)
--- a/frontends/src/jp/cmd_account.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,132 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -"""This module permits to manage XMPP accounts using in-band registration (XEP-0077)""" - -from sat_frontends.jp.constants import Const as C -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ -from sat_frontends.jp import base -from sat_frontends.tools import jid - -__commands__ = ["Account"] - - -class AccountCreate(base.CommandBase): - - def __init__(self, host): - super(AccountCreate, self).__init__(host, 'create', use_profile=False, use_verbose=True, help=_(u'create a XMPP account')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('jid', type=base.unicode_decoder, help=_(u'jid to create')) - self.parser.add_argument('password', type=base.unicode_decoder, help=_(u'password of the account')) - self.parser.add_argument('-p', '--profile', type=base.unicode_decoder, help=_(u"create a profile to use this account (default: don't create profile)")) - self.parser.add_argument('-e', '--email', type=base.unicode_decoder, default="", help=_(u"email (usage depends of XMPP server)")) - self.parser.add_argument('-H', '--host', type=base.unicode_decoder, default="", help=_(u"server host (IP address or domain, default: use localhost)")) - self.parser.add_argument('-P', '--port', type=int, default=0, help=_(u"server port (IP address or domain, default: use localhost)")) - - def _setParamCb(self): - self.host.bridge.setParam("Password", self.args.password, "Connection", profile_key=self.args.profile, callback=self.host.quit, errback=self.errback) - - def _session_started(self, dummy): - self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.args.profile, callback=self._setParamCb, errback=self.errback) - - def _profileCreateCb(self): - self.disp(_(u"profile created"), 1) - self.host.bridge.profileStartSession(self.args.password, self.args.profile, callback=self._session_started, errback=self.errback) - - def _profileCreateEb(self, failure_): - self.disp(_(u"Can't create profile {profile} to associate with jid {jid}: {msg}").format( - profile = self.args.profile, - jid = self.args.jid, - msg = failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def accountNewCb(self): - self.disp(_(u"XMPP account created"), 1) - if self.args.profile is not None: - self.disp(_(u"creating profile"), 2) - self.host.bridge.profileCreate(self.args.profile, self.args.password, "", callback=self._profileCreateCb, errback=self._profileCreateEb) - else: - self.host.quit() - - def accountNewEb(self, failure_): - self.disp(_(u"Can't create new account on server {host} with jid {jid}: {msg}").format( - host = self.args.host or u"localhost", - jid = self.args.jid, - msg = failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.inBandAccountNew(self.args.jid, self.args.password, self.args.email, self.args.host, self.args.port, - callback=self.accountNewCb, errback=self.accountNewEb) - - - -class AccountModify(base.CommandBase): - - def __init__(self, host): - super(AccountModify, self).__init__(host, 'modify', help=_(u'change password for XMPP account')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('password', type=base.unicode_decoder, help=_(u'new XMPP password')) - - def start(self): - self.host.bridge.inBandPasswordChange(self.args.password, self.args.profile, - callback=self.host.quit, errback=self.errback) - - -class AccountDelete(base.CommandBase): - - def __init__(self, host): - super(AccountDelete, self).__init__(host, 'delete', help=_(u'delete a XMPP account')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('-f', '--force', action='store_true', help=_(u'delete account without confirmation')) - - def _got_jid(self, jid_str): - jid_ = jid.JID(jid_str) - if not self.args.force: - message = (u"You are about to delete the XMPP account with jid {jid_}\n" - u"This is the XMPP account of profile \"{profile}\"\n" - u"Are you sure that you want to delete this account ?".format( - jid_ = jid_, - profile=self.profile - )) - res = raw_input("{} (y/N)? ".format(message)) - if res not in ("y", "Y"): - self.disp(_(u"Account deletion cancelled")) - self.host.quit(2) - self.host.bridge.inBandUnregister(jid_.domain, self.args.profile, - callback=self.host.quit, errback=self.errback) - - def start(self): - self.host.bridge.asyncGetParamA("JabberID", "Connection", profile_key=self.profile, - callback=self._got_jid, errback=self.errback) - - -class Account(base.CommandBase): - subcommands = (AccountCreate, AccountModify, AccountDelete) - - def __init__(self, host): - super(Account, self).__init__(host, 'account', use_profile=False, help=(u'XMPP account management'))
--- a/frontends/src/jp/cmd_adhoc.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import base -from sat.core.i18n import _ -from functools import partial -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import xmlui_manager - -__commands__ = ["AdHoc"] - -FLAG_LOOP = 'LOOP' -MAGIC_BAREJID = '@PROFILE_BAREJID@' - - -class Remote(base.CommandBase): - def __init__(self, host): - super(Remote, self).__init__(host, 'remote', use_verbose=True, help=_(u'remote control a software')) - - def add_parser_options(self): - self.parser.add_argument("software", type=str, help=_(u"software name")) - self.parser.add_argument("-j", "--jids", type=base.unicode_decoder, nargs='*', default=[], help=_(u"jids allowed to use the command")) - self.parser.add_argument("-g", "--groups", type=base.unicode_decoder, nargs='*', default=[], help=_(u"groups allowed to use the command")) - self.parser.add_argument("--forbidden-groups", type=base.unicode_decoder, nargs='*', default=[], help=_(u"groups that are *NOT* allowed to use the command")) - self.parser.add_argument("--forbidden-jids", type=base.unicode_decoder, nargs='*', default=[], help=_(u"jids that are *NOT* allowed to use the command")) - self.parser.add_argument("-l", "--loop", action="store_true", help=_(u"loop on the commands")) - - def start(self): - name = self.args.software.lower() - flags = [] - magics = {jid for jid in self.args.jids if jid.count('@')>1} - magics.add(MAGIC_BAREJID) - jids = set(self.args.jids).difference(magics) - if self.args.loop: - flags.append(FLAG_LOOP) - bus_name, methods = self.host.bridge.adHocDBusAddAuto(name, jids, self.args.groups, magics, - self.args.forbidden_jids, self.args.forbidden_groups, - flags, self.profile) - if not bus_name: - self.disp(_("No bus name found"), 1) - return - self.disp(_("Bus name found: [%s]" % bus_name), 1) - for method in methods: - path, iface, command = method - self.disp(_("Command found: (path:%(path)s, iface: %(iface)s) [%(command)s]" % {'path': path, - 'iface': iface, - 'command': command - }),1) - - -class Run(base.CommandBase): - """Run an Ad-Hoc command""" - - def __init__(self, host): - super(Run, self).__init__(host, 'run', use_verbose=True, help=_(u'run an Ad-Hoc command')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument('-j', '--jid', type=base.unicode_decoder, default=u'', help=_(u"jid of the service (default: profile's server")) - self.parser.add_argument("-S", "--submit", action='append_const', const=xmlui_manager.SUBMIT, dest='workflow', help=_(u"submit form/page")) - self.parser.add_argument("-f", - "--field", - type=base.unicode_decoder, - action='append', - nargs=2, - dest='workflow', - metavar=(u"KEY", u"VALUE"), - help=_(u"field value")) - self.parser.add_argument('node', type=base.unicode_decoder, nargs='?', default=u'', help=_(u"node of the command (default: list commands)")) - - def adHocRunCb(self, xmlui_raw): - xmlui = xmlui_manager.create(self.host, xmlui_raw) - workflow = self.args.workflow - xmlui.show(workflow) - if not workflow: - if xmlui.type == 'form': - xmlui.submitForm() - else: - self.host.quit() - - def start(self): - self.host.bridge.adHocRun( - self.args.jid, - self.args.node, - self.profile, - callback=self.adHocRunCb, - errback=partial(self.errback, - msg=_(u"can't get ad-hoc commands list: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class List(base.CommandBase): - """Run an Ad-Hoc command""" - - def __init__(self, host): - super(List, self).__init__(host, 'list', use_verbose=True, help=_(u'list Ad-Hoc commands of a service')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument('-j', '--jid', type=base.unicode_decoder, default=u'', help=_(u"jid of the service (default: profile's server")) - - def adHocListCb(self, xmlui_raw): - xmlui = xmlui_manager.create(self.host, xmlui_raw) - xmlui.readonly = True - xmlui.show() - self.host.quit() - - def start(self): - self.host.bridge.adHocList( - self.args.jid, - self.profile, - callback=self.adHocListCb, - errback=partial(self.errback, - msg=_(u"can't get ad-hoc commands list: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class AdHoc(base.CommandBase): - subcommands = (Run, List, Remote) - - def __init__(self, host): - super(AdHoc, self).__init__(host, 'ad-hoc', use_profile=False, help=_('Ad-hoc commands'))
--- a/frontends/src/jp/cmd_avatar.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -import os -import os.path -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat.tools import config -import subprocess - - -__commands__ = ["Avatar"] -DISPLAY_CMD = ['xv', 'display', 'gwenview', 'showtell'] - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__(host, 'set', use_verbose=True, help=_('set avatar of the profile')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("image_path", type=str, help=_("path to the image to upload")) - - def start(self): - """Send files to jabber contact""" - path = self.args.image_path - if not os.path.exists(path): - self.disp(_(u"file [{}] doesn't exist !").format(path), error=True) - self.host.quit(1) - path = os.path.abspath(path) - self.host.bridge.avatarSet(path, self.profile, callback=self._avatarCb, errback=self._avatarEb) - - def _avatarCb(self): - self.disp(_("avatar has been set"), 1) - self.host.quit() - - def _avatarEb(self, failure_): - self.disp(_("error while uploading avatar: {msg}").format(msg=failure_), error=True) - self.host.quit(C.EXIT_ERROR) - - -class Get(base.CommandBase): - - def __init__(self, host): - super(Get, self).__init__(host, 'get', use_verbose=True, help=_('retrieve avatar of an entity')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("jid", type=base.unicode_decoder, help=_("entity")) - self.parser.add_argument("-s", "--show", action="store_true", help=_(u"show avatar")) - - def showImage(self, path): - sat_conf = config.parseMainConf() - cmd = config.getConfig(sat_conf, 'jp', 'image_cmd') - cmds = [cmd] + DISPLAY_CMD if cmd else DISPLAY_CMD - for cmd in cmds: - try: - ret = subprocess.call([cmd] + [path]) - except OSError: - pass - else: - if ret == 0: - break - else: - # didn't worked with commands, we try our luck with webbrowser - # in some cases, webbrowser can actually open the associated display program - import webbrowser - webbrowser.open(path) - - def _avatarGetCb(self, avatar_path): - if not avatar_path: - self.disp(_(u"No avatar found."), 1) - self.host.quit(C.EXIT_NOT_FOUND) - - self.disp(avatar_path) - if self.args.show: - self.showImage(avatar_path) - - self.host.quit() - - def _avatarGetEb(self, failure_): - self.disp(_("error while getting avatar: {msg}").format(msg=failure_), error=True) - self.host.quit(C.EXIT_ERROR) - - def start(self): - self.host.bridge.avatarGet(self.args.jid, False, False, self.profile, callback=self._avatarGetCb, errback=self._avatarGetEb) - - -class Avatar(base.CommandBase): - subcommands = (Set, Get) - - def __init__(self, host): - super(Avatar, self).__init__(host, 'avatar', use_profile=False, help=_('avatar uploading/retrieving'))
--- a/frontends/src/jp/cmd_blog.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,691 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat.tools.common.ansi import ANSI as A -from sat.tools.common import data_objects -from sat.tools.common import uri -from sat.tools import config -from ConfigParser import NoSectionError, NoOptionError -from functools import partial -import json -import sys -import os.path -import os -import time -import tempfile -import subprocess -import codecs -from sat.tools.common import data_format - -__commands__ = ["Blog"] - -SYNTAX_XHTML = u'xhtml' -# extensions to use with known syntaxes -SYNTAX_EXT = { - '': 'txt', # used when the syntax is not found - SYNTAX_XHTML: "xhtml", - "markdown": "md" - } - - -CONF_SYNTAX_EXT = u'syntax_ext_dict' -BLOG_TMP_DIR = u"blog" -# key to remove from metadata tmp file if they exist -KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated') - -URL_REDIRECT_PREFIX = 'url_redirect_' -INOTIFY_INSTALL = '"pip install inotify"' -MB_KEYS = (u"id", - u"url", - u"atom_id", - u"updated", - u"published", - u"language", - u"comments", # this key is used for all comments* keys - u"tags", # this key is used for all tag* keys - u"author", - u"author_jid", - u"author_email", - u"author_jid_verified", - u"content", - u"content_xhtml", - u"title", - u"title_xhtml", - ) -OUTPUT_OPT_NO_HEADER = u'no-header' - - -def guessSyntaxFromPath(host, sat_conf, path): - """Return syntax guessed according to filename extension - - @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration - @param path(str): path to the content file - @return(unicode): syntax to use - """ - # we first try to guess syntax with extension - ext = os.path.splitext(path)[1][1:] # we get extension without the '.' - if ext: - for k,v in SYNTAX_EXT.iteritems(): - if k and ext == v: - return k - - # if not found, we use current syntax - return host.bridge.getParamA("Syntax", "Composition", "value", host.profile) - - -class BlogPublishCommon(object): - """handle common option for publising commands (Set and Edit)""" - - def add_parser_options(self): - self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item")) - self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item")) - self.parser.add_argument("-C", "--comments", action='store_true', help=_(u"enable comments")) - self.parser.add_argument("-S", '--syntax', type=base.unicode_decoder, help=_(u"syntax to use (default: get profile's default syntax)")) - - def setMbDataContent(self, content, mb_data): - if self.args.syntax is None: - # default syntax has been used - mb_data['content_rich'] = content - elif self.current_syntax == SYNTAX_XHTML: - mb_data['content_xhtml'] = content - else: - mb_data['content_xhtml'] = self.host.bridge.syntaxConvert(content, self.current_syntax, SYNTAX_XHTML, False, self.profile) - - def setMbDataFromArgs(self, mb_data): - """set microblog metadata according to command line options - - if metadata already exist, it will be overwritten - """ - mb_data['allow_comments'] = C.boolConst(self.args.comments) - if self.args.tag: - data_format.iter2dict('tag', self.args.tag, mb_data, check_conflict=False) - if self.args.title is not None: - mb_data['title'] = self.args.title - - -class Set(base.CommandBase, BlogPublishCommon): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, - help=_(u'publish a new blog item or update an existing one')) - BlogPublishCommon.__init__(self) - self.need_loop=True - - def add_parser_options(self): - BlogPublishCommon.add_parser_options(self) - - def mbSendCb(self): - self.disp(u"Item published") - self.host.quit(C.EXIT_OK) - - def start(self): - self.pubsub_item = self.args.item - mb_data = {} - self.setMbDataFromArgs(mb_data) - content = codecs.getreader('utf-8')(sys.stdin).read() - self.setMbDataContent(content, mb_data) - - self.host.bridge.mbSend( - self.args.service, - self.args.node, - mb_data, - self.profile, - callback=self.exitCb, - errback=partial(self.errback, - msg=_(u"can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Get(base.CommandBase): - TEMPLATE = u"blog/articles.html" - - def __init__(self, host): - extra_outputs = {'default': self.default_output, - 'fancy': self.fancy_output} - base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS}, - use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)')) - self.need_loop=True - - def add_parser_options(self): - # TODO: a key(s) argument to select keys to display - self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys', - help=_(u"microblog data key(s) to display (default: depend of verbosity)")) - # TODO: add MAM filters - - def template_data_mapping(self, data): - return {u'items': data_objects.BlogItems(data)} - - def format_comments(self, item, keys): - comments_data = data_format.dict2iterdict(u'comments', item, (u'node', u'service'), pop=True) - lines = [] - for data in comments_data: - lines.append(data[u'comments']) - for k in (u'node', u'service'): - if OUTPUT_OPT_NO_HEADER in self.args.output_opts: - header = u'' - else: - header = C.A_HEADER + k + u': ' + A.RESET - lines.append(header + data[k]) - return u'\n'.join(lines) - - def format_tags(self, item, keys): - tags = data_format.dict2iter('tag', item, pop=True) - return u', '.join(tags) - - def format_updated(self, item, keys): - return self.format_time(item['updated']) - - def format_published(self, item, keys): - return self.format_time(item['published']) - - def format_url(self, item, keys): - return uri.buildXMPPUri(u'pubsub', - subtype=u'microblog', - path=self.metadata[u'service'], - node=self.metadata[u'node'], - item=item[u'id']) - - def get_keys(self): - """return keys to display according to verbosity or explicit key request""" - verbosity = self.args.verbose - if self.args.keys: - if not set(MB_KEYS).issuperset(self.args.keys): - self.disp(u"following keys are invalid: {invalid}.\n" - u"Valid keys are: {valid}.".format( - invalid = u', '.join(set(self.args.keys).difference(MB_KEYS)), - valid = u', '.join(sorted(MB_KEYS))), - error=True) - self.host.quit(C.EXIT_BAD_ARG) - return self.args.keys - else: - if verbosity == 0: - return (u'title', u'content') - elif verbosity == 1: - return (u"title", u"tags", u"author", u"author_jid", u"author_email", u"author_jid_verified", u"published", u"updated", u"content") - else: - return MB_KEYS - - def default_output(self, data): - """simple key/value output""" - items, self.metadata = data - keys = self.get_keys() - - # k_cb use format_[key] methods for complex formattings - k_cb = {} - for k in keys: - try: - callback = getattr(self, "format_" + k) - except AttributeError: - pass - else: - k_cb[k] = callback - for idx, item in enumerate(items): - for k in keys: - if k not in item and k not in k_cb: - continue - if OUTPUT_OPT_NO_HEADER in self.args.output_opts: - header = '' - else: - header = u"{k_fmt}{key}:{k_fmt_e} {sep}".format( - k_fmt = C.A_HEADER, - key = k, - k_fmt_e = A.RESET, - sep = u'\n' if 'content' in k else u'') - value = k_cb[k](item, keys) if k in k_cb else item[k] - self.disp(header + value) - # we want a separation line after each item but the last one - if idx < len(items)-1: - print(u'') - - def format_time(self, timestamp): - """return formatted date for timestamp - - @param timestamp(str,int,float): unix timestamp - @return (unicode): formatted date - """ - fmt = u"%d/%m/%Y %H:%M:%S" - return time.strftime(fmt, time.localtime(float(timestamp))) - - def fancy_output(self, data): - """display blog is a nice to read way - - this output doesn't use keys filter - """ - # thanks to http://stackoverflow.com/a/943921 - rows, columns = map(int, os.popen('stty size', 'r').read().split()) - items, metadata = data - verbosity = self.args.verbose - sep = A.color(A.FG_BLUE, columns * u'▬') - if items: - print(u'\n' + sep + '\n') - - for idx, item in enumerate(items): - title = item.get(u'title') - if verbosity > 0: - author = item[u'author'] - published, updated = item[u'published'], item.get('updated') - else: - author = published = updated = None - if verbosity > 1: - tags = list(data_format.dict2iter('tag', item, pop=True)) - else: - tags = None - content = item.get(u'content') - - if title: - print(A.color(A.BOLD, A.FG_CYAN, item[u'title'])) - meta = [] - if author: - meta.append(A.color(A.FG_YELLOW, author)) - if published: - meta.append(A.color(A.FG_YELLOW, u'on ', self.format_time(published))) - if updated != published: - meta.append(A.color(A.FG_YELLOW, u'(updated on ', self.format_time(updated), u')')) - print(u' '.join(meta)) - if tags: - print(A.color(A.FG_MAGENTA, u', '.join(tags))) - if (title or tags) and content: - print("") - if content: - self.disp(content) - - print(u'\n' + sep + '\n') - - - def mbGetCb(self, mb_result): - self.output(mb_result) - self.host.quit(C.EXIT_OK) - - def mbGetEb(self, failure_): - self.disp(u"can't get blog items: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.mbGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - {}, - self.profile, - callback=self.mbGetCb, - errback=self.mbGetEb) - - -class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, - use_draft=True, use_verbose=True, help=_(u'edit an existing or new blog post')) - BlogPublishCommon.__init__(self) - common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) - - @property - def current_syntax(self): - if self._current_syntax is None: - self._current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) - return self._current_syntax - - def add_parser_options(self): - BlogPublishCommon.add_parser_options(self) - self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel")) - - def buildMetadataFile(self, content_file_path, mb_data=None): - """Build a metadata file using json - - The file is named after content_file_path, with extension replaced by _metadata.json - @param content_file_path(str): path to the temporary file which will contain the body - @param mb_data(dict, None): microblog metadata (for existing items) - @return (tuple[dict, str]): merged metadata put originaly in metadata file - and path to temporary metadata file - """ - # we first construct metadata from edited item ones and CLI argumments - # or re-use the existing one if it exists - meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF - if os.path.exists(meta_file_path): - self.disp(u"Metadata file already exists, we re-use it") - try: - with open(meta_file_path, 'rb') as f: - mb_data = json.load(f) - except (OSError, IOError, ValueError) as e: - self.disp(u"Can't read existing metadata file at {path}, aborting: {reason}".format( - path=meta_file_path, reason=e), error=True) - self.host.quit(1) - else: - mb_data = {} if mb_data is None else mb_data.copy() - - # in all cases, we want to remove unwanted keys - for key in KEY_TO_REMOVE_METADATA: - try: - del mb_data[key] - except KeyError: - pass - # and override metadata with command-line arguments - self.setMbDataFromArgs(mb_data) - - # then we create the file and write metadata there, as JSON dict - # XXX: if we port jp one day on Windows, O_BINARY may need to be added here - with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC,0o600), 'w+b') as f: - # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters - unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True) - f.write(unicode_dump.encode('utf-8')) - - return mb_data, meta_file_path - - def edit(self, content_file_path, content_file_obj, - mb_data=None): - """Edit the file contening the content using editor, and publish it""" - # we first create metadata file - meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data) - - # do we need a preview ? - if self.args.preview: - self.disp(u"Preview requested, launching it", 1) - # we redirect outputs to /dev/null to avoid console pollution in editor - # if user wants to see messages, (s)he can call "blog preview" directly - DEVNULL = open(os.devnull, 'wb') - subprocess.Popen([sys.argv[0], "blog", "preview", "--inotify", "true", "-p", self.profile, content_file_path], stdout=DEVNULL, stderr=subprocess.STDOUT) - - # we launch editor - self.runEditor("blog_editor_args", content_file_path, content_file_obj, meta_file_path=meta_file_path, meta_ori=meta_ori) - - def publish(self, content, mb_data): - self.setMbDataContent(content, mb_data) - - if self.pubsub_item is not None: - mb_data['id'] = self.pubsub_item - - self.host.bridge.mbSend(self.pubsub_service, self.pubsub_node, mb_data, self.profile) - self.disp(u"Blog item published") - - - def getTmpSuff(self): - # we get current syntax to determine file extension - return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT['']) - - def getItemData(self, service, node, item): - items = [item] if item is not None else [] - mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0] - try: - content = mb_data['content_xhtml'] - except KeyError: - content = mb_data['content'] - if content: - content = self.host.bridge.syntaxConvert(content, 'text', SYNTAX_XHTML, False, self.profile) - if content and self.current_syntax != SYNTAX_XHTML: - content = self.host.bridge.syntaxConvert(content, SYNTAX_XHTML, self.current_syntax, False, self.profile) - if content and self.current_syntax == SYNTAX_XHTML: - try: - from lxml import etree - except ImportError: - self.disp(_(u"You need lxml to edit pretty XHTML")) - else: - parser = etree.XMLParser(remove_blank_text=True) - root = etree.fromstring(content, parser) - content = etree.tostring(root, encoding=unicode, pretty_print=True) - - return content, mb_data, mb_data['id'] - - def start(self): - # if there are user defined extension, we use them - SYNTAX_EXT.update(config.getConfig(self.sat_conf, 'jp', CONF_SYNTAX_EXT, {})) - self._current_syntax = self.args.syntax - if self._current_syntax is not None: - try: - self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax) - except Exception as e: - if "NotFound" in unicode(e): # FIXME: there is not good way to check bridge errors - self.parser.error(_(u"unknown syntax requested ({syntax})").format(syntax=self.args.syntax)) - else: - raise e - - self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath() - - self.edit(content_file_path, content_file_obj, mb_data=mb_data) - - -class Preview(base.CommandBase): - # TODO: need to be rewritten with template output - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content')) - - def add_parser_options(self): - self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview")) - self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file")) - - def showPreview(self): - # we implement showPreview here so we don't have to import webbrowser and urllib - # when preview is not used - url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) - self.webbrowser.open_new_tab(url) - - def _launchPreviewExt(self, cmd_line, opt_name): - url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) - args = common.parse_args(self.host, cmd_line, url=url, preview_file=self.preview_file_path) - if not args: - self.disp(u"Couln't find command in \"{name}\", abording".format(name=opt_name), error=True) - self.host.quit(1) - subprocess.Popen(args) - - def openPreviewExt(self): - self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd") - - def updatePreviewExt(self): - self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd") - - def updateContent(self): - with open(self.content_file_path, 'rb') as f: - content = f.read().decode('utf-8-sig') - if content and self.syntax != SYNTAX_XHTML: - # we use safe=True because we want to have a preview as close as possible to what the - # people will see - content = self.host.bridge.syntaxConvert(content, self.syntax, SYNTAX_XHTML, True, self.profile) - - xhtml = (u'<html xmlns="http://www.w3.org/1999/xhtml">' + - u'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /></head>'+ - '<body>{}</body>' + - u'</html>').format(content) - - with open(self.preview_file_path, 'wb') as f: - f.write(xhtml.encode('utf-8')) - - def start(self): - import webbrowser - import urllib - self.webbrowser, self.urllib = webbrowser, urllib - - if self.args.inotify != 'false': - try: - import inotify.adapters - import inotify.constants - from inotify.calls import InotifyError - except ImportError: - if self.args.inotify == 'auto': - inotify = None - self.disp(u'inotify module not found, deactivating feature. You can install it with {install}'.format(install=INOTIFY_INSTALL)) - else: - self.disp(u"inotify not found, can't activate the feature! Please install it with {install}".format(install=INOTIFY_INSTALL), error=True) - self.host.quit(1) - else: - # we deactivate logging in inotify, which is quite annoying - try: - inotify.adapters._LOGGER.setLevel(40) - except AttributeError: - self.disp(u"Logger doesn't exists, inotify may have chanded", error=True) - else: - inotify=None - - sat_conf = config.parseMainConf() - SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) - - try: - self.open_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_open_cmd", Exception) - except (NoOptionError, NoSectionError): - self.open_cb_cmd = None - open_cb = self.showPreview - else: - open_cb = self.openPreviewExt - - self.update_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_update_cmd", self.open_cb_cmd) - if self.update_cb_cmd is None: - update_cb = self.showPreview - else: - update_cb = self.updatePreviewExt - - # which file do we need to edit? - if self.args.file == 'current': - self.content_file_path = self.getCurrentFile(sat_conf, self.profile) - else: - self.content_file_path = os.path.abspath(self.args.file) - - self.syntax = self.guessSyntaxFromPath(sat_conf, self.content_file_path) - - - # at this point the syntax is converted, we can display the preview - preview_file = tempfile.NamedTemporaryFile(suffix='.xhtml', delete=False) - self.preview_file_path = preview_file.name - preview_file.close() - self.updateContent() - - if inotify is None: - # XXX: we don't delete file automatically because browser need it (and webbrowser.open can return before it is read) - self.disp(u'temporary file created at {}\nthis file will NOT BE DELETED AUTOMATICALLY, please delete it yourself when you have finished'.format(self.preview_file_path)) - open_cb() - else: - open_cb() - i = inotify.adapters.Inotify(block_duration_s=60) # no need for 1 s duraction, inotify drive actions here - - def add_watch(): - i.add_watch(self.content_file_path, mask=inotify.constants.IN_CLOSE_WRITE | - inotify.constants.IN_DELETE_SELF | - inotify.constants.IN_MOVE_SELF) - add_watch() - - try: - for event in i.event_gen(): - if event is not None: - self.disp(u"Content updated", 1) - if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]): - self.disp(u"{} event catched, changing the watch".format(", ".join(event[1])), 2) - try: - add_watch() - except InotifyError: - # if the new file is not here yet we can have an error - # as a workaround, we do a little rest - time.sleep(1) - add_watch() - self.updateContent() - update_cb() - except InotifyError: - self.disp(u"Can't catch inotify events, as the file been deleted?", error=True) - finally: - os.unlink(self.preview_file_path) - try: - i.remove_watch(self.content_file_path) - except InotifyError: - pass - - -class Import(base.CommandAnswering): - def __init__(self, host): - super(Import, self).__init__(host, 'import', use_pubsub=True, use_progress=True, help=_(u'import an external blog')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("importer", type=base.unicode_decoder, nargs='?', help=_(u"importer name, nothing to display importers list")) - self.parser.add_argument('--host', type=base.unicode_decoder, help=_(u"original blog host")) - self.parser.add_argument('--no-images-upload', action='store_true', help=_(u"do *NOT* upload images (default: do upload images)")) - self.parser.add_argument('--upload-ignore-host', help=_(u"do not upload images from this host (default: upload all images)")) - self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate for uploads")) - self.parser.add_argument('-o', '--option', action='append', nargs=2, default=[], metavar=(u'NAME', u'VALUE'), - help=_(u"importer specific options (see importer description)")) - self.parser.add_argument("location", type=base.unicode_decoder, nargs='?', - help=_(u"importer data location (see importer description), nothing to show importer description")) - - def onProgressStarted(self, metadata): - self.disp(_(u'Blog upload started'),2) - - def onProgressFinished(self, metadata): - self.disp(_(u'Blog uploaded successfully'),2) - redirections = {k[len(URL_REDIRECT_PREFIX):]:v for k,v in metadata.iteritems() - if k.startswith(URL_REDIRECT_PREFIX)} - if redirections: - conf = u'\n'.join([ - u'url_redirections_profile = {}'.format(self.profile), - u"url_redirections_dict = {}".format( - # we need to add ' ' before each new line and to double each '%' for ConfigParser - u'\n '.join(json.dumps(redirections, indent=1, separators=(',',': ')).replace(u'%', u'%%').split(u'\n'))), - ]) - self.disp(_(u'\nTo redirect old URLs to new ones, put the following lines in your sat.conf file, in [libervia] section:\n\n{conf}'.format(conf=conf))) - - def onProgressError(self, error_msg): - self.disp(_(u'Error while uploading blog: {}').format(error_msg),error=True) - - def error(self, failure): - self.disp(_("Error while trying to upload a blog: {reason}").format(reason=failure), error=True) - self.host.quit(1) - - def start(self): - if self.args.location is None: - for name in ('option', 'service', 'no_images_upload'): - if getattr(self.args, name): - self.parser.error(_(u"{name} argument can't be used without location argument").format(name=name)) - if self.args.importer is None: - self.disp(u'\n'.join([u'{}: {}'.format(name, desc) for name, desc in self.host.bridge.blogImportList()])) - else: - try: - short_desc, long_desc = self.host.bridge.blogImportDesc(self.args.importer) - except Exception as e: - msg = [l for l in unicode(e).split('\n') if l][-1] # we only keep the last line - self.disp(msg) - self.host.quit(1) - else: - self.disp(u"{name}: {short_desc}\n\n{long_desc}".format(name=self.args.importer, short_desc=short_desc, long_desc=long_desc)) - self.host.quit() - else: - # we have a location, an import is requested - options = {key: value for key, value in self.args.option} - if self.args.host: - options['host'] = self.args.host - if self.args.ignore_tls_errors: - options['ignore_tls_errors'] = C.BOOL_TRUE - if self.args.no_images_upload: - options['upload_images'] = C.BOOL_FALSE - if self.args.upload_ignore_host: - self.parser.error(u"upload-ignore-host option can't be used when no-images-upload is set") - elif self.args.upload_ignore_host: - options['upload_ignore_host'] = self.args.upload_ignore_host - def gotId(id_): - self.progress_id = id_ - self.host.bridge.blogImport(self.args.importer, self.args.location, options, self.args.service, self.args.node, self.profile, - callback=gotId, errback=self.error) - - -class Blog(base.CommandBase): - subcommands = (Set, Get, Edit, Preview, Import) - - def __init__(self, host): - super(Blog, self).__init__(host, 'blog', use_profile=False, help=_('blog/microblog management'))
--- a/frontends/src/jp/cmd_bookmarks.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import base -from sat.core.i18n import _ - -__commands__ = ["Bookmarks"] - -STORAGE_LOCATIONS = ('local', 'private', 'pubsub') -TYPES = ('muc', 'url') - -class BookmarksCommon(base.CommandBase): - """Class used to group common options of bookmarks subcommands""" - - def add_parser_options(self, location_default='all'): - self.parser.add_argument('-l', '--location', type=str, choices=(location_default,) + STORAGE_LOCATIONS, default=location_default, help=_("storage location (default: %(default)s)")) - self.parser.add_argument('-t', '--type', type=str, choices=TYPES, default=TYPES[0], help=_("bookmarks type (default: %(default)s)")) - - def _errback(self, failure): - print (("Something went wrong: [%s]") % failure) - self.host.quit(1) - -class BookmarksList(BookmarksCommon): - - def __init__(self, host): - super(BookmarksList, self).__init__(host, 'list', help=_('list bookmarks')) - - def start(self): - data = self.host.bridge.bookmarksList(self.args.type, self.args.location, self.host.profile) - mess = [] - for location in STORAGE_LOCATIONS: - if not data[location]: - continue - loc_mess = [] - loc_mess.append(u"%s:" % location) - book_mess = [] - for book_link, book_data in data[location].items(): - name = book_data.get('name') - autojoin = book_data.get('autojoin', 'false') == 'true' - nick = book_data.get('nick') - book_mess.append(u"\t%s[%s%s]%s" % ((name+' ') if name else '', - book_link, - u' (%s)' % nick if nick else '', - u' (*)' if autojoin else '')) - loc_mess.append(u'\n'.join(book_mess)) - mess.append(u'\n'.join(loc_mess)) - - print u'\n\n'.join(mess) - - -class BookmarksRemove(BookmarksCommon): - - def __init__(self, host): - super(BookmarksRemove, self).__init__(host, 'remove', help=_('remove a bookmark')) - self.need_loop = True - - def add_parser_options(self): - super(BookmarksRemove, self).add_parser_options() - self.parser.add_argument('bookmark', type=base.unicode_decoder, help=_('jid (for muc bookmark) or url of to remove')) - - def start(self): - self.host.bridge.bookmarksRemove(self.args.type, self.args.bookmark, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback) - - -class BookmarksAdd(BookmarksCommon): - - def __init__(self, host): - super(BookmarksAdd, self).__init__(host, 'add', help=_('add a bookmark')) - self.need_loop = True - - def add_parser_options(self): - super(BookmarksAdd, self).add_parser_options(location_default='auto') - self.parser.add_argument('bookmark', type=base.unicode_decoder, help=_('jid (for muc bookmark) or url of to remove')) - self.parser.add_argument('-n', '--name', type=base.unicode_decoder, help=_("bookmark name")) - muc_group = self.parser.add_argument_group(_('MUC specific options')) - muc_group.add_argument('-N', '--nick', type=base.unicode_decoder, help=_('nickname')) - muc_group.add_argument('-a', '--autojoin', action='store_true', help=_('join room on profile connection')) - - def start(self): - if self.args.type == 'url' and (self.args.autojoin or self.args.nick is not None): - # XXX: Argparse doesn't seem to manage this case, any better way ? - print _(u"You can't use --autojoin or --nick with --type url") - self.host.quit(1) - data = {} - if self.args.autojoin: - data['autojoin'] = 'true' - if self.args.nick is not None: - data['nick'] = self.args.nick - if self.args.name is not None: - data['name'] = self.args.name - self.host.bridge.bookmarksAdd(self.args.type, self.args.bookmark, data, self.args.location, self.host.profile, callback = lambda: self.host.quit(), errback=self._errback) - - -class Bookmarks(base.CommandBase): - subcommands = (BookmarksList, BookmarksRemove, BookmarksAdd) - - def __init__(self, host): - super(Bookmarks, self).__init__(host, 'bookmarks', use_profile=False, help=_('manage bookmarks'))
--- a/frontends/src/jp/cmd_debug.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,160 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat.tools.common.ansi import ANSI as A -import json - -__commands__ = ["Debug"] - - -class BridgeCommon(object): - - def evalArgs(self): - if self.args.arg: - try: - return eval(u'[{}]'.format(u",".join(self.args.arg))) - except SyntaxError as e: - self.disp(u"Can't evaluate arguments: {mess}\n{text}\n{offset}^".format( - mess=e, - text=e.text.decode('utf-8'), - offset=u" "*(e.offset-1) - ), error=True) - self.host.quit(C.EXIT_BAD_ARG) - else: - return [] - - -class Method(base.CommandBase, BridgeCommon): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'method', help=_(u'call a bridge method')) - BridgeCommon.__init__(self) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("method", type=str, help=_(u"name of the method to execute")) - self.parser.add_argument("arg", type=base.unicode_decoder, nargs="*", help=_(u"argument of the method")) - - def method_cb(self, ret=None): - if ret is not None: - self.disp(unicode(ret)) - self.host.quit() - - def method_eb(self, failure): - self.disp(_(u"Error while executing {}: {}".format(self.args.method, failure)), error=True) - self.host.quit(C.EXIT_ERROR) - - def start(self): - method = getattr(self.host.bridge, self.args.method) - args = self.evalArgs() - try: - method(*args, profile=self.profile, callback=self.method_cb, errback=self.method_eb) - except TypeError: - # maybe the method doesn't need a profile ? - try: - method(*args, callback=self.method_cb, errback=self.method_eb) - except TypeError: - self.method_eb(_(u"bad arguments")) - - -class Signal(base.CommandBase, BridgeCommon): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'signal', help=_(u'send a fake signal from backend')) - BridgeCommon.__init__(self) - - def add_parser_options(self): - self.parser.add_argument("signal", type=str, help=_(u"name of the signal to send")) - self.parser.add_argument("arg", type=base.unicode_decoder, nargs="*", help=_(u"argument of the signal")) - - def start(self): - args = self.evalArgs() - json_args = json.dumps(args) - # XXX: we use self.args.profile and not self.profile - # because we want the raw profile_key (so plugin handle C.PROF_KEY_NONE) - self.host.bridge.debugFakeSignal(self.args.signal, json_args, self.args.profile) - - -class Bridge(base.CommandBase): - subcommands = (Method, Signal) - - def __init__(self, host): - super(Bridge, self).__init__(host, 'bridge', use_profile=False, help=_('bridge s(t)imulation')) - - -class Monitor(base.CommandBase): - - def __init__(self, host): - super(Monitor, self).__init__(host, - 'monitor', - use_verbose=True, - use_profile=False, - use_output=C.OUTPUT_XML, - help=_('monitor XML stream')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument("-d", "--direction", choices=('in', 'out', 'both'), default='both', help=_(u"stream direction filter")) - - def printXML(self, direction, xml_data, profile): - if self.args.direction == 'in' and direction != 'IN': - return - if self.args.direction == 'out' and direction != 'OUT': - return - verbosity = self.host.verbosity - if not xml_data.strip(): - if verbosity <= 2: - return - whiteping = True - else: - whiteping = False - - if verbosity: - profile_disp = u' ({})'.format(profile) if verbosity>1 else u'' - if direction == 'IN': - self.disp(A.color(A.BOLD, A.FG_YELLOW, '<<<===== IN ====', A.FG_WHITE, profile_disp)) - else: - self.disp(A.color(A.BOLD, A.FG_CYAN, '==== OUT ====>>>', A.FG_WHITE, profile_disp)) - if whiteping: - self.disp('[WHITESPACE PING]') - else: - try: - self.output(xml_data) - except Exception: - # initial stream is not valid XML, - # in this case we print directly to data - # FIXME: we should test directly lxml.etree.XMLSyntaxError - # but importing lxml directly here is not clean - # should be wrapped in a custom Exception - self.disp(xml_data) - self.disp(u'') - - def start(self): - self.host.bridge.register_signal('xmlLog', self.printXML, 'plugin') - - -class Debug(base.CommandBase): - subcommands = (Bridge, Monitor) - - def __init__(self, host): - super(Debug, self).__init__(host, 'debug', use_profile=False, help=_('debugging tools'))
--- a/frontends/src/jp/cmd_event.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,416 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat.tools.common.ansi import ANSI as A -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from functools import partial -from dateutil import parser as du_parser -import calendar -import time - -__commands__ = ["Event"] - -OUTPUT_OPT_TABLE = u'table' - -# TODO: move date parsing to base, it may be useful for other commands - - -class Get(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, - host, - 'get', - use_output=C.OUTPUT_DICT, - use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, - use_verbose=True, - help=_(u'get event data')) - self.need_loop=True - - def add_parser_options(self): - pass - - def eventInviteeGetCb(self, result): - event_date, event_data = result - event_data['date'] = event_date - self.output(event_data) - self.host.quit() - - def start(self): - self.host.bridge.eventGet( - self.args.service, - self.args.node, - self.args.item, - self.profile, - callback=self.eventInviteeGetCb, - errback=partial(self.errback, - msg=_(u"can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class EventBase(object): - - def add_parser_options(self): - self.parser.add_argument("-i", "--id", type=base.unicode_decoder, default=u'', help=_(u"ID of the PubSub Item")) - self.parser.add_argument("-d", "--date", type=unicode, help=_(u"date of the event")) - self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields', - metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set")) - - def parseFields(self): - return dict(self.args.fields) if self.args.fields else {} - - def parseDate(self): - if self.args.date: - try: - date = int(self.args.date) - except ValueError: - try: - date_time = du_parser.parse(self.args.date, dayfirst=not (u'-' in self.args.date)) - except ValueError as e: - self.parser.error(_(u"Can't parse date: {msg}").format(msg=e)) - if date_time.tzinfo is None: - date = calendar.timegm(date_time.timetuple()) - else: - date = time.mktime(date_time.timetuple()) - else: - date = -1 - return date - - -class Create(EventBase, base.CommandBase): - def __init__(self, host): - super(Create, self).__init__(host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_('create or replace event')) - EventBase.__init__(self) - self.need_loop=True - - def eventCreateCb(self, node): - self.disp(_(u'Event created successfuly on node {node}').format(node=node)) - self.host.quit() - - def start(self): - fields = self.parseFields() - date = self.parseDate() - self.host.bridge.eventCreate( - date, - fields, - self.args.service, - self.args.node, - self.args.id, - self.profile, - callback=self.eventCreateCb, - errback=partial(self.errback, - msg=_(u"can't create event: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Modify(EventBase, base.CommandBase): - def __init__(self, host): - super(Modify, self).__init__(host, 'modify', use_pubsub=True, pubsub_flags={C.NODE}, help=_('modify an existing event')) - EventBase.__init__(self) - self.need_loop=True - - def start(self): - fields = self.parseFields() - date = 0 if not self.args.date else self.parseDate() - self.host.bridge.eventModify( - self.args.service, - self.args.node, - self.args.id, - date, - fields, - self.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_(u"can't update event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class InviteeGet(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, - host, - 'get', - use_output=C.OUTPUT_DICT, - use_pubsub=True, pubsub_flags={C.NODE}, - use_verbose=True, - help=_(u'get event attendance')) - self.need_loop=True - - def add_parser_options(self): - pass - - def eventInviteeGetCb(self, event_data): - self.output(event_data) - self.host.quit() - - def start(self): - self.host.bridge.eventInviteeGet( - self.args.service, - self.args.node, - self.profile, - callback=self.eventInviteeGetCb, - errback=partial(self.errback, - msg=_(u"can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class InviteeSet(base.CommandBase): - def __init__(self, host): - super(InviteeSet, self).__init__(host, 'set', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_('set event attendance')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields', - metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set")) - - def start(self): - fields = dict(self.args.fields) if self.args.fields else {} - self.host.bridge.eventInviteeSet( - self.args.service, - self.args.node, - fields, - self.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_(u"can't set event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class InviteesList(base.CommandBase): - - def __init__(self, host): - extra_outputs = {'default': self.default_output} - base.CommandBase.__init__(self, - host, - 'list', - use_output=C.OUTPUT_DICT_DICT, - extra_outputs=extra_outputs, - use_pubsub=True, pubsub_flags={C.NODE}, - use_verbose=True, - help=_(u'get event attendance')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument('-m', '--missing', action='store_true', help=_(u'show missing people (invited but no R.S.V.P. so far)')) - self.parser.add_argument('-R', '--no-rsvp', action='store_true', help=_(u"don't show people which gave R.S.V.P.")) - - def _attend_filter(self, attend, row): - if attend == u'yes': - attend_color = C.A_SUCCESS - elif attend == u'no': - attend_color = C.A_FAILURE - else: - attend_color = A.FG_WHITE - return A.color(attend_color, attend) - - def _guests_filter(self, guests): - return u'(' + unicode(guests) + ')' if guests else u'' - - def default_output(self, event_data): - data = [] - attendees_yes = 0 - attendees_maybe = 0 - attendees_no = 0 - attendees_missing = 0 - guests = 0 - guests_maybe = 0 - for jid_, jid_data in event_data.iteritems(): - jid_data[u'jid'] = jid_ - try: - guests_int = int(jid_data['guests']) - except (ValueError, KeyError): - pass - attend = jid_data.get(u'attend',u'') - if attend == 'yes': - attendees_yes += 1 - guests += guests_int - elif attend == 'maybe': - attendees_maybe += 1 - guests_maybe += guests_int - elif attend == 'no': - attendees_no += 1 - jid_data[u'guests'] = '' - else: - attendees_missing += 1 - jid_data[u'guests'] = '' - data.append(jid_data) - - show_table = OUTPUT_OPT_TABLE in self.args.output_opts - - table = common.Table.fromDict(self.host, - data, - (u'nick',) + ((u'jid',) if self.host.verbosity else ()) + (u'attend', 'guests'), - headers=None, - filters = { u'nick': A.color(C.A_HEADER, u'{}' if show_table else u'{} '), - u'jid': u'{}' if show_table else u'{} ', - u'attend': self._attend_filter, - u'guests': u'{}' if show_table else self._guests_filter, - }, - defaults = { u'nick': u'', - u'attend': u'', - u'guests': 1 - } - ) - if show_table: - table.display() - else: - table.display_blank(show_header=False, col_sep=u'') - - if not self.args.no_rsvp: - self.disp(u'') - self.disp(A.color( - C.A_SUBHEADER, - _(u'Attendees: '), - A.RESET, - unicode(len(data)), - _(u' ('), - C.A_SUCCESS, - _(u'yes: '), - unicode(attendees_yes), - A.FG_WHITE, - _(u', maybe: '), - unicode(attendees_maybe), - u', ', - C.A_FAILURE, - _(u'no: '), - unicode(attendees_no), - A.RESET, - u')' - )) - self.disp(A.color(C.A_SUBHEADER, _(u'confirmed guests: '), A.RESET, unicode(guests))) - self.disp(A.color(C.A_SUBHEADER, _(u'unconfirmed guests: '), A.RESET, unicode(guests_maybe))) - self.disp(A.color(C.A_SUBHEADER, _(u'total: '), A.RESET, unicode(guests+guests_maybe))) - if attendees_missing: - self.disp('') - self.disp(A.color(C.A_SUBHEADER, _(u'missing people (no reply): '), A.RESET, unicode(attendees_missing))) - - def eventInviteesListCb(self, event_data, prefilled_data): - """fill nicknames and keep only requested people - - @param event_data(dict): R.S.V.P. answers - @param prefilled_data(dict): prefilled data with all people - only filled if --missing is used - """ - if self.args.no_rsvp: - for jid_ in event_data: - # if there is a jid in event_data - # it must be there in prefilled_data too - # so no need to check for KeyError - del prefilled_data[jid_] - else: - # we replace empty dicts for existing people with R.S.V.P. data - prefilled_data.update(event_data) - - # we get nicknames for everybody, make it easier for organisers - for jid_, data in prefilled_data.iteritems(): - id_data = self.host.bridge.identityGet(jid_, self.profile) - data[u'nick'] = id_data.get(u'nick', u'') - - self.output(prefilled_data) - self.host.quit() - - def getList(self, prefilled_data={}): - self.host.bridge.eventInviteesList( - self.args.service, - self.args.node, - self.profile, - callback=partial(self.eventInviteesListCb, - prefilled_data=prefilled_data), - errback=partial(self.errback, - msg=_(u"can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def psNodeAffiliationsGetCb(self, affiliations): - # we fill all affiliations with empty data - # answered one will be filled in eventInviteesListCb - # we only consider people with "publisher" affiliation as invited, creators are not, and members can just observe - prefilled = {jid_: {} for jid_, affiliation in affiliations.iteritems() if affiliation in (u'publisher',)} - self.getList(prefilled) - - def start(self): - if self.args.no_rsvp and not self.args.missing: - self.parser.error(_(u"you need to use --missing if you use --no-rsvp")) - if self.args.missing: - self.host.bridge.psNodeAffiliationsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeAffiliationsGetCb, - errback=partial(self.errback, - msg=_(u"can't get event data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - else: - self.getList() - - -class InviteeInvite(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'invite', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'invite someone to the event through email')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-e", "--email", action="append", type=base.unicode_decoder, default=[], help='email(s) to send the invitation to') - self.parser.add_argument("-N", "--name", type=base.unicode_decoder, default='', help='name of the invitee') - self.parser.add_argument("-H", "--host-name", type=base.unicode_decoder, default='', help='name of the host') - self.parser.add_argument("-l", "--lang", type=base.unicode_decoder, default='', help='main language spoken by the invitee') - self.parser.add_argument("-U", "--url-template", type=base.unicode_decoder, default='', help='template to construct the URL') - self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, default='', help='subject of the invitation email (default: generic subject)') - self.parser.add_argument("-b", "--body", type=base.unicode_decoder, default='', help='body of the invitation email (default: generic body)') - - def start(self): - email = self.args.email[0] if self.args.email else None - emails_extra = self.args.email[1:] - - self.host.bridge.eventInvite( - self.args.service, - self.args.node, - self.args.item, - email, - emails_extra, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url_template, - self.args.subject, - self.args.body, - self.args.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_(u"can't create invitation: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Invitee(base.CommandBase): - subcommands = (InviteeGet, InviteeSet, InviteesList, InviteeInvite) - - def __init__(self, host): - super(Invitee, self).__init__(host, 'invitee', use_profile=False, help=_(u'manage invities')) - - -class Event(base.CommandBase): - subcommands = (Get, Create, Modify, Invitee) - - def __init__(self, host): - super(Event, self).__init__(host, 'event', use_profile=False, help=_('event management'))
--- a/frontends/src/jp/cmd_file.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,503 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -import sys -import os -import os.path -import tarfile -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat_frontends.tools import jid -from sat.tools.common.ansi import ANSI as A -import tempfile -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -from functools import partial -import json - -__commands__ = ["File"] - - -class Send(base.CommandBase): - def __init__(self, host): - super(Send, self).__init__(host, 'send', use_progress=True, use_verbose=True, help=_('send a file to a contact')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("files", type=str, nargs='+', metavar='file', help=_(u"a list of file")) - self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid")) - self.parser.add_argument("-b", "--bz2", action="store_true", help=_(u"make a bzip2 tarball")) - self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory where the file must be stored")) - self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file")) - self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=(u"name to use (DEFAULT: use source file name)")) - - def start(self): - """Send files to jabber contact""" - self.send_files() - - def onProgressStarted(self, metadata): - self.disp(_(u'File copy started'),2) - - def onProgressFinished(self, metadata): - self.disp(_(u'File sent successfully'),2) - - def onProgressError(self, error_msg): - if error_msg == C.PROGRESS_ERROR_DECLINED: - self.disp(_(u'The file has been refused by your contact')) - else: - self.disp(_(u'Error while sending file: {}').format(error_msg),error=True) - - def gotId(self, data, file_): - """Called when a progress id has been received - - @param pid(unicode): progress id - @param file_(str): file path - """ - #FIXME: this show progress only for last progress_id - self.disp(_(u"File request sent to {jid}".format(jid=self.full_dest_jid)), 1) - try: - self.progress_id = data['progress'] - except KeyError: - # TODO: if 'xmlui' key is present, manage xmlui message display - self.disp(_(u"Can't send file to {jid}".format(jid=self.full_dest_jid)), error=True) - self.host.quit(2) - - def error(self, failure): - self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True) - self.host.quit(1) - - def send_files(self): - for file_ in self.args.files: - if not os.path.exists(file_): - self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True) - self.host.quit(1) - if not self.args.bz2 and os.path.isdir(file_): - self.disp(_(u"[{}] is a dir ! Please send files inside or use compression").format(file_)) - self.host.quit(1) - - self.full_dest_jid = self.host.get_full_jid(self.args.jid) - extra = {} - if self.args.path: - extra[u'path'] = self.args.path - if self.args.namespace: - extra[u'namespace'] = self.args.namespace - - if self.args.bz2: - with tempfile.NamedTemporaryFile('wb', delete=False) as buf: - self.host.addOnQuitCallback(os.unlink, buf.name) - self.disp(_(u"bz2 is an experimental option, use with caution")) - #FIXME: check free space - self.disp(_(u"Starting compression, please wait...")) - sys.stdout.flush() - bz2 = tarfile.open(mode="w:bz2", fileobj=buf) - archive_name = u'{}.tar.bz2'.format(os.path.basename(self.args.files[0]) or u'compressed_files') - for file_ in self.args.files: - self.disp(_(u"Adding {}").format(file_), 1) - bz2.add(file_) - bz2.close() - self.disp(_(u"Done !"), 1) - - self.host.bridge.fileSend(self.full_dest_jid, buf.name, self.args.name or archive_name, '', extra, self.profile, - callback=lambda pid, file_=buf.name: self.gotId(pid, file_), errback=self.error) - else: - for file_ in self.args.files: - path = os.path.abspath(file_) - self.host.bridge.fileSend(self.full_dest_jid, path, self.args.name, '', extra, self.profile, - callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error) - - -class Request(base.CommandBase): - - def __init__(self, host): - super(Request, self).__init__(host, 'request', use_progress=True, use_verbose=True, help=_('request a file from a contact')) - self.need_loop=True - - @property - def filename(self): - return self.args.name or self.args.hash or u"output" - - def add_parser_options(self): - self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid")) - self.parser.add_argument("-D", "--dest", type=base.unicode_decoder, help=_(u"destination path where the file will be saved (default: [current_dir]/[name|hash])")) - self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"name of the file")) - self.parser.add_argument("-H", "--hash", type=base.unicode_decoder, default=u'', help=_(u"hash of the file")) - self.parser.add_argument("-a", "--hash-algo", type=base.unicode_decoder, default=u'sha-256', help=_(u"hash algorithm use for --hash (default: sha-256)")) - self.parser.add_argument("-d", "--path", type=base.unicode_decoder, help=(u"path to the directory containing the file")) - self.parser.add_argument("-N", "--namespace", type=base.unicode_decoder, help=(u"namespace of the file")) - self.parser.add_argument("-f", "--force", action='store_true', help=_(u"overwrite existing file without confirmation")) - - def onProgressStarted(self, metadata): - self.disp(_(u'File copy started'),2) - - def onProgressFinished(self, metadata): - self.disp(_(u'File received successfully'),2) - - def onProgressError(self, error_msg): - if error_msg == C.PROGRESS_ERROR_DECLINED: - self.disp(_(u'The file request has been refused')) - else: - self.disp(_(u'Error while requesting file: {}').format(error_msg), error=True) - - def gotId(self, progress_id): - """Called when a progress id has been received - - @param progress_id(unicode): progress id - """ - self.progress_id = progress_id - - def error(self, failure): - self.disp(_("Error while trying to send a file: {reason}").format(reason=failure), error=True) - self.host.quit(1) - - def start(self): - if not self.args.name and not self.args.hash: - self.parser.error(_(u'at least one of --name or --hash must be provided')) - # extra = dict(self.args.extra) - if self.args.dest: - path = os.path.abspath(os.path.expanduser(self.args.dest)) - if os.path.isdir(path): - path = os.path.join(path, self.filename) - else: - path = os.path.abspath(self.filename) - - if os.path.exists(path) and not self.args.force: - message = _(u'File {path} already exists! Do you want to overwrite?').format(path=path) - confirm = raw_input(u"{} (y/N) ".format(message).encode('utf-8')) - if confirm not in (u"y", u"Y"): - self.disp(_(u"file request cancelled")) - self.host.quit(2) - - self.full_dest_jid = self.host.get_full_jid(self.args.jid) - extra = {} - if self.args.path: - extra[u'path'] = self.args.path - if self.args.namespace: - extra[u'namespace'] = self.args.namespace - self.host.bridge.fileJingleRequest(self.full_dest_jid, - path, - self.args.name, - self.args.hash, - self.args.hash_algo if self.args.hash else u'', - extra, - self.profile, - callback=self.gotId, - errback=partial(self.errback, - msg=_(u"can't request file: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Receive(base.CommandAnswering): - - def __init__(self, host): - super(Receive, self).__init__(host, 'receive', use_progress=True, use_verbose=True, help=_('wait for a file to be sent by a contact')) - self._overwrite_refused = False # True when one overwrite as already been refused - self.action_callbacks = {C.META_TYPE_FILE: self.onFileAction, - C.META_TYPE_OVERWRITE: self.onOverwriteAction} - - def onProgressStarted(self, metadata): - self.disp(_(u'File copy started'),2) - - def onProgressFinished(self, metadata): - self.disp(_(u'File received successfully'),2) - if metadata.get('hash_verified', False): - try: - self.disp(_(u'hash checked: {algo}:{checksum}').format( - algo=metadata['hash_algo'], - checksum=metadata['hash']), - 1) - except KeyError: - self.disp(_(u'hash is checked but hash value is missing', 1), error=True) - else: - self.disp(_(u"hash can't be verified"), 1) - - def onProgressError(self, error_msg): - self.disp(_(u'Error while receiving file: {}').format(error_msg),error=True) - - def getXmluiId(self, action_data): - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the futur - # TODO: XMLUI module - try: - xml_ui = action_data['xmlui'] - except KeyError: - self.disp(_(u"Action has no XMLUI"), 1) - else: - ui = ET.fromstring(xml_ui.encode('utf-8')) - xmlui_id = ui.get('submit') - if not xmlui_id: - self.disp(_(u"Invalid XMLUI received"), error=True) - return xmlui_id - - def onFileAction(self, action_data, action_id, security_limit, profile): - xmlui_id = self.getXmluiId(action_data) - if xmlui_id is None: - return self.host.quitFromSignal(1) - try: - from_jid = jid.JID(action_data['meta_from_jid']) - except KeyError: - self.disp(_(u"Ignoring action without from_jid data"), 1) - return - try: - progress_id = action_data['meta_progress_id'] - except KeyError: - self.disp(_(u"ignoring action without progress id"), 1) - return - - if not self.bare_jids or from_jid.bare in self.bare_jids: - if self._overwrite_refused: - self.disp(_(u"File refused because overwrite is needed"), error=True) - self.host.bridge.launchAction(xmlui_id, {'cancelled': C.BOOL_TRUE}, profile_key=profile) - return self.host.quitFromSignal(2) - self.progress_id = progress_id - xmlui_data = {'path': self.path} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - - def onOverwriteAction(self, action_data, action_id, security_limit, profile): - xmlui_id = self.getXmluiId(action_data) - if xmlui_id is None: - return self.host.quitFromSignal(1) - try: - progress_id = action_data['meta_progress_id'] - except KeyError: - self.disp(_(u"ignoring action without progress id"), 1) - return - self.disp(_(u"Overwriting needed"), 1) - - if progress_id == self.progress_id: - if self.args.force: - self.disp(_(u"Overwrite accepted"), 2) - else: - self.disp(_(u"Refused to overwrite"), 2) - self._overwrite_refused = True - - xmlui_data = {'answer': C.boolConst(self.args.force)} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - - def add_parser_options(self): - self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_(u'jids accepted (accept everything if none is specified)')) - self.parser.add_argument("-m", "--multiple", action="store_true", help=_(u"accept multiple files (you'll have to stop manually)")) - self.parser.add_argument("-f", "--force", action="store_true", help=_(u"force overwritting of existing files (/!\\ name is choosed by sender)")) - self.parser.add_argument("--path", default='.', metavar='DIR', help=_(u"destination path (default: working directory)")) - - def start(self): - self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] - self.path = os.path.abspath(self.args.path) - if not os.path.isdir(self.path): - self.disp(_(u"Given path is not a directory !", error=True)) - self.host.quit(2) - if self.args.multiple: - self.host.quit_on_progress_end = False - self.disp(_(u"waiting for incoming file request"),2) - - -class Upload(base.CommandBase): - - def __init__(self, host): - super(Upload, self).__init__(host, 'upload', use_progress=True, use_verbose=True, help=_('upload a file')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("file", type=str, help=_("file to upload")) - self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of upload component (nothing to autodetect)")) - self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate")) - - def onProgressStarted(self, metadata): - self.disp(_(u'File upload started'),2) - - def onProgressFinished(self, metadata): - self.disp(_(u'File uploaded successfully'),2) - try: - url = metadata['url'] - except KeyError: - self.disp(u'download URL not found in metadata') - else: - self.disp(_(u'URL to retrieve the file:'),1) - # XXX: url is display alone on a line to make parsing easier - self.disp(url) - - def onProgressError(self, error_msg): - self.disp(_(u'Error while uploading file: {}').format(error_msg),error=True) - - def gotId(self, data, file_): - """Called when a progress id has been received - - @param pid(unicode): progress id - @param file_(str): file path - """ - try: - self.progress_id = data['progress'] - except KeyError: - # TODO: if 'xmlui' key is present, manage xmlui message display - self.disp(_(u"Can't upload file"), error=True) - self.host.quit(2) - - def error(self, failure): - self.disp(_("Error while trying to upload a file: {reason}").format(reason=failure), error=True) - self.host.quit(1) - - def start(self): - file_ = self.args.file - if not os.path.exists(file_): - self.disp(_(u"file [{}] doesn't exist !").format(file_), error=True) - self.host.quit(1) - if os.path.isdir(file_): - self.disp(_(u"[{}] is a dir! Can't upload a dir").format(file_)) - self.host.quit(1) - - self.full_dest_jid = self.host.get_full_jid(self.args.jid) if self.args.jid is not None else '' - options = {} - if self.args.ignore_tls_errors: - options['ignore_tls_errors'] = C.BOOL_TRUE - - path = os.path.abspath(file_) - self.host.bridge.fileUpload(path, '', self.full_dest_jid, options, self.profile, callback=lambda pid, file_=file_: self.gotId(pid, file_), errback=self.error) - - -class ShareList(base.CommandBase): - - def __init__(self, host): - extra_outputs = {'default': self.default_output} - super(ShareList, self).__init__(host, 'list', use_output=C.OUTPUT_LIST_DICT, extra_outputs=extra_outputs, help=_(u'retrieve files shared by an entity'), use_verbose=True) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-d", "--path", default=u'', help=_(u"path to the directory containing the files")) - self.parser.add_argument("jid", type=base.unicode_decoder, nargs='?', help=_("jid of sharing entity (nothing to check our own jid)")) - - def file_gen(self, files_data): - for file_data in files_data: - yield file_data[u'name'] - yield file_data.get(u'size', '') - yield file_data.get(u'hash','') - - def _name_filter(self, name, row): - if row.type == C.FILE_TYPE_DIRECTORY: - return A.color(C.A_DIRECTORY, name) - elif row.type == C.FILE_TYPE_FILE: - return A.color(C.A_FILE, name) - else: - self.disp(_(u'unknown file type: {type}').format(type=row.type), error=True) - return name - - def _size_filter(self, size, row): - if not size: - return u'' - size = int(size) - # cf. https://stackoverflow.com/a/1094933 (thanks) - suffix = u'o' - for unit in [u'', u'Ki', u'Mi', u'Gi', u'Ti', u'Pi', u'Ei', u'Zi']: - if abs(size) < 1024.0: - return A.color(A.BOLD, u"{:.2f}".format(size), unit, suffix) - size /= 1024.0 - - return A.color(A.BOLD, u"{:.2f}".format(size), u'Yi', suffix) - - def default_output(self, files_data): - """display files a way similar to ls""" - files_data.sort(key=lambda d: d['name'].lower()) - show_header = False - if self.verbosity == 0: - headers = (u'name', u'type') - elif self.verbosity == 1: - headers = (u'name', u'type', u'size') - elif self.verbosity > 1: - show_header = True - headers = (u'name', u'type', u'size', u'hash') - table = common.Table.fromDict(self.host, - files_data, - headers, - filters={u'name': self._name_filter, - u'size': self._size_filter}, - defaults={u'size': u'', - u'hash': u''}, - ) - table.display_blank(show_header=show_header, hide_cols=['type']) - - def _FISListCb(self, files_data): - self.output(files_data) - self.host.quit() - - def start(self): - self.host.bridge.FISList( - self.args.jid, - self.args.path, - {}, - self.profile, - callback=self._FISListCb, - errback=partial(self.errback, - msg=_(u"can't retrieve shared files: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class SharePath(base.CommandBase): - - def __init__(self, host): - super(SharePath, self).__init__(host, 'path', help=_(u'share a file or directory'), use_verbose=True) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default=u'', help=_(u"virtual name to use (default: use directory/file name)")) - perm_group = self.parser.add_mutually_exclusive_group() - perm_group.add_argument("-j", "--jid", type=base.unicode_decoder, action='append', dest="jids", default=[], help=_(u"jid of contacts allowed to retrieve the files")) - perm_group.add_argument("--public", action='store_true', help=_(u"share publicly the file(s) (/!\\ *everybody* will be able to access them)")) - self.parser.add_argument("path", type=base.unicode_decoder, help=_(u"path to a file or directory to share")) - - - def _FISSharePathCb(self, name): - self.disp(_(u'{path} shared under the name "{name}"').format( - path = self.path, - name = name)) - self.host.quit() - - def start(self): - self.path = os.path.abspath(self.args.path) - if self.args.public: - access = {u'read': {u'type': u'public'}} - else: - jids = self.args.jids - if jids: - access = {u'read': {u'type': 'whitelist', - u'jids': jids}} - else: - access = {} - self.host.bridge.FISSharePath( - self.args.name, - self.path, - json.dumps(access, ensure_ascii=False), - self.profile, - callback=self._FISSharePathCb, - errback=partial(self.errback, - msg=_(u"can't share path: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Share(base.CommandBase): - subcommands = (ShareList, SharePath) - - def __init__(self, host): - super(Share, self).__init__(host, 'share', use_profile=False, help=_(u'files sharing management')) - - -class File(base.CommandBase): - subcommands = (Send, Request, Receive, Upload, Share) - - def __init__(self, host): - super(File, self).__init__(host, 'file', use_profile=False, help=_(u'files sending/receiving/management'))
--- a/frontends/src/jp/cmd_forums.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,163 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat.tools.common.ansi import ANSI as A -from functools import partial -import json - -__commands__ = ["Forums"] - -FORUMS_TMP_DIR = u"forums" - - -class Edit(base.CommandBase, common.BaseEdit): - use_items=False - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, use_draft=True, use_verbose=True, help=_(u'edit forums')) - common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-k", "--key", type=base.unicode_decoder, default=u'', - help=_(u"forum key (DEFAULT: default forums)")) - - def getTmpSuff(self): - """return suffix used for content file""" - return u'json' - - def forumsSetCb(self): - self.disp(_(u'forums have been edited'), 1) - self.host.quit() - - def publish(self, forums_raw): - self.host.bridge.forumsSet( - forums_raw, - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsSetCb, - errback=partial(self.errback, - msg=_(u"can't set forums: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def forumsGetCb(self, forums_json): - content_file_obj, content_file_path = self.getTmpFile() - forums_json = forums_json.strip() - if forums_json: - # we loads and dumps to have pretty printed json - forums = json.loads(forums_json) - json.dump(forums, content_file_obj, ensure_ascii=False, indent=4) - content_file_obj.seek(0) - self.runEditor("forums_editor_args", content_file_path, content_file_obj) - - def forumsGetEb(self, failure_): - # FIXME: error handling with bridge is broken, need to be properly fixed - if failure_.condition == u'item-not-found': - self.forumsGetCb(u'') - else: - self.errback(failure_, - msg=_(u"can't get forums structure: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.forumsGet( - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsGetCb, - errback=self.forumsGetEb) - - -class Get(base.CommandBase): - - def __init__(self, host): - extra_outputs = {'default': self.default_output} - base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, use_pubsub=True, use_verbose=True, help=_(u'get forums structure')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-k", "--key", type=base.unicode_decoder, default=u'', - help=_(u"forum key (DEFAULT: default forums)")) - - def default_output(self, forums, level=0): - for forum in forums: - keys = list(forum.keys()) - keys.sort() - try: - keys.remove(u'title') - except ValueError: - pass - else: - keys.insert(0, u'title') - try: - keys.remove(u'sub-forums') - except ValueError: - pass - else: - keys.append(u'sub-forums') - - for key in keys: - value = forum[key] - if key == 'sub-forums': - self.default_output(value, level+1) - else: - if self.host.verbosity < 1 and key != u'title': - continue - head_color = C.A_LEVEL_COLORS[level % len(C.A_LEVEL_COLORS)] - self.disp(A.color(level * 4 * u' ', - head_color, - key, - A.RESET, - u': ', - value)) - - def forumsGetCb(self, forums_raw): - if not forums_raw: - self.disp(_(u'no schema found'), 1) - self.host.quit(1) - forums = json.loads(forums_raw) - self.output(forums) - self.host.quit() - - def start(self): - self.host.bridge.forumsGet( - self.args.service, - self.args.node, - self.args.key, - self.profile, - callback=self.forumsGetCb, - errback=partial(self.errback, - msg=_(u"can't get forums: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - - -class Forums(base.CommandBase): - subcommands = (Get, Edit) - - def __init__(self, host): - super(Forums, self).__init__(host, 'forums', use_profile=False, help=_(u'Forums structure edition'))
--- a/frontends/src/jp/cmd_identity.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from functools import partial - -__commands__ = ["Identity"] - -# TODO: move date parsing to base, it may be useful for other commands - - -class Get(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, - host, - 'get', - use_output=C.OUTPUT_DICT, - use_verbose=True, - help=_(u'get identity data')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"entity to check")) - - def identityGetCb(self, data): - self.output(data) - self.host.quit() - - def start(self): - jid_ = self.host.check_jids([self.args.jid])[0] - self.host.bridge.identityGet( - jid_, - self.profile, - callback=self.identityGetCb, - errback=partial(self.errback, - msg=_(u"can't get identity data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__(host, 'set', help=_('modify an existing event')) - - def add_parser_options(self): - self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields', - metavar=(u"KEY", u"VALUE"), required=True, help=_(u"identity field(s) to set")) - self.need_loop=True - - def start(self): - fields = dict(self.args.fields) - self.host.bridge.identitySet( - fields, - self.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_(u"can't set identity data data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Identity(base.CommandBase): - subcommands = (Get, Set) - - def __init__(self, host): - super(Identity, self).__init__(host, 'identity', use_profile=False, help=_('identity management'))
--- a/frontends/src/jp/cmd_info.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,193 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import base -from sat.core.i18n import _ -from sat.tools.common.ansi import ANSI as A -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common - -__commands__ = ["Info"] - - -class Disco(base.CommandBase): - - def __init__(self, host): - extra_outputs = {'default': self.default_output} - super(Disco, self).__init__(host, 'disco', use_output='complex', extra_outputs=extra_outputs, help=_('service discovery')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument(u"jid", type=base.unicode_decoder, help=_(u"entity to discover")) - self.parser.add_argument(u"-t", u"--type", type=str, choices=('infos', 'items', 'both'), default='both', help=_(u"type of data to discover")) - self.parser.add_argument(u"-n", u"--node", type=base.unicode_decoder, default=u'', help=_(u"node to use")) - self.parser.add_argument(u"-C", u"--no-cache", dest='use_cache', action="store_false", help=_(u"ignore cache")) - - def start(self): - self.get_infos = self.args.type in ('infos', 'both') - self.get_items = self.args.type in ('items', 'both') - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - if not self.get_infos: - self.gotInfos(None, jid) - else: - self.host.bridge.discoInfos(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda infos: self.gotInfos(infos, jid), errback=self.error) - - def error(self, failure): - print (_("Error while doing discovery [%s]") % failure) - self.host.quit(1) - - def gotInfos(self, infos, jid): - if not self.get_items: - self.gotItems(infos, None) - else: - self.host.bridge.discoItems(jid, node=self.args.node, use_cache=self.args.use_cache, profile_key=self.host.profile, callback=lambda items: self.gotItems(infos, items), errback=self.error) - - def gotItems(self, infos, items): - data = {} - - if self.get_infos: - features, identities, extensions = infos - features.sort() - identities.sort(key=lambda identity: identity[2]) - data.update({ - u'features': features, - u'identities': identities, - u'extensions': extensions}) - - if self.get_items: - items.sort(key=lambda item: item[2]) - data[u'items'] = items - - self.output(data) - self.host.quit() - - def default_output(self, data): - features = data.get(u'features', []) - identities = data.get(u'identities', []) - extensions = data.get(u'extensions', {}) - items = data.get(u'items', []) - - identities_table = common.Table(self.host, - identities, - headers=(_(u'category'), - _(u'type'), - _(u'name')), - use_buffer=True) - - extensions_tpl = [] - extensions_types = extensions.keys() - extensions_types.sort() - for type_ in extensions_types: - fields = [] - for field in extensions[type_]: - field_lines = [] - data, values = field - data_keys = data.keys() - data_keys.sort() - for key in data_keys: - field_lines.append(A.color(u'\t', C.A_SUBHEADER, key, data[key])) - for value in values: - field_lines.append(A.color(u'\t', A.BOLD, value)) - fields.append(u'\n'.join(field_lines)) - extensions_tpl.append(u'{type_}\n{fields}'.format(type_=type_, - fields='\n\n'.join(fields))) - - items_table = common.Table(self.host, - items, - headers=(_(u'entity'), - _(u'node'), - _(u'name')), - use_buffer=True) - - template = [] - if features: - template.append(A.color(C.A_HEADER, _(u"Features")) + u"\n\n{features}") - if identities: - template.append(A.color(C.A_HEADER, _(u"Identities")) + u"\n\n{identities}") - if extensions: - template.append(A.color(C.A_HEADER, _(u"Extensions")) + u"\n\n{extensions}") - if items: - template.append(A.color(C.A_HEADER, _(u"Items")) + u"\n\n{items}") - - print u"\n\n".join(template).format(features = u'\n'.join(features), - identities = identities_table.display().string, - extensions = u'\n'.join(extensions_tpl), - items = items_table.display().string, - ) - - -class Version(base.CommandBase): - - def __init__(self, host): - super(Version, self).__init__(host, 'version', help=_('software version')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("jid", type=str, help=_("Entity to request")) - - def start(self): - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - self.host.bridge.getSoftwareVersion(jid, self.host.profile, callback=self.gotVersion, errback=self.error) - - def error(self, failure): - print (_("Error while trying to get version [%s]") % failure) - self.host.quit(1) - - def gotVersion(self, data): - infos = [] - name, version, os = data - if name: - infos.append(_("Client name: %s") % name) - if version: - infos.append(_("Client version: %s") % version) - if os: - infos.append(_("Operating System: %s") % os) - - print "\n".join(infos) - self.host.quit() - - -class Session(base.CommandBase): - - def __init__(self, host): - super(Session, self).__init__(host, 'session', use_output='dict', help=_('running session')) - self.need_loop=True - - def add_parser_options(self): - pass - - def start(self): - self.host.bridge.sessionInfosGet(self.host.profile, callback=self._sessionInfoGetCb, errback=self._sessionInfoGetEb) - - def _sessionInfoGetCb(self, data): - self.output(data) - self.host.quit() - - def _sessionInfoGetEb(self, error_data): - self.disp(_(u'Error getting session infos: {}').format(error_data), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - -class Info(base.CommandBase): - subcommands = (Disco, Version, Session) - - def __init__(self, host): - super(Info, self).__init__(host, 'info', use_profile=False, help=_('Get various pieces of information on entities'))
--- a/frontends/src/jp/cmd_input.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,227 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat.core import exceptions -from sat_frontends.jp.constants import Const as C -from sat.tools.common.ansi import ANSI as A -import subprocess -import argparse -import sys - -__commands__ = ["Input"] -OPT_STDIN = 'stdin' -OPT_SHORT = 'short' -OPT_LONG = 'long' -OPT_POS = 'positional' -OPT_IGNORE = 'ignore' -OPT_TYPES = (OPT_STDIN, OPT_SHORT, OPT_LONG, OPT_POS, OPT_IGNORE) -OPT_EMPTY_SKIP = 'skip' -OPT_EMPTY_IGNORE = 'ignore' -OPT_EMPTY_CHOICES = (OPT_EMPTY_SKIP, OPT_EMPTY_IGNORE) - - -class InputCommon(base.CommandBase): - - def __init__(self, host, name, help): - base.CommandBase.__init__(self, host, name, use_verbose=True, use_profile=False, help=help) - self.idx = 0 - self.reset() - - def reset(self): - self.args_idx = 0 - self._stdin = [] - self._opts = [] - self._pos = [] - self._values_ori = [] - - def add_parser_options(self): - self.parser.add_argument("--encoding", default='utf-8', help=_(u"encoding of the input data")) - self.parser.add_argument("-i", "--stdin", action='append_const', const=(OPT_STDIN, None), dest='arguments', help=_(u"standard input")) - self.parser.add_argument("-s", "--short", type=self.opt(OPT_SHORT), action='append', dest='arguments', help=_(u"short option")) - self.parser.add_argument("-l", "--long", type=self.opt(OPT_LONG), action='append', dest='arguments', help=_(u"long option")) - self.parser.add_argument("-p", "--positional", type=self.opt(OPT_POS), action='append', dest='arguments', help=_(u"positional argument")) - self.parser.add_argument("-x", "--ignore", action='append_const', const=(OPT_IGNORE, None), dest='arguments', help=_(u"ignore value")) - self.parser.add_argument("-D", "--debug", action='store_true', help=_(u"don't actually run commands but echo what would be launched")) - self.parser.add_argument("--log", type=argparse.FileType('wb'), help=_(u"log stdout to FILE")) - self.parser.add_argument("--log-err", type=argparse.FileType('wb'), help=_(u"log stderr to FILE")) - self.parser.add_argument("command", nargs=argparse.REMAINDER) - - def opt(self, type_): - return lambda s: (type_, s) - - def addValue(self, value): - """add a parsed value according to arguments sequence""" - self._values_ori.append(value) - arguments = self.args.arguments - try: - arg_type, arg_name = arguments[self.args_idx] - except IndexError: - self.disp(_(u"arguments in input data and in arguments sequence don't match"), error=True) - self.host.quit(C.EXIT_DATA_ERROR) - self.args_idx += 1 - while self.args_idx < len(arguments): - next_arg = arguments[self.args_idx] - if next_arg[0] not in OPT_TYPES: - # value will not be used if False or None, so we skip filter - if value not in (False, None): - # we have a filter - filter_type, filter_arg = arguments[self.args_idx] - value = self.filter(filter_type, filter_arg, value) - else: - break - self.args_idx += 1 - - if value is None: - # we ignore this argument - return - - if value is False: - # we skip the whole row - if self.args.debug: - self.disp(A.color(C.A_SUBHEADER, _(u'values: '), A.RESET, u', '.join(self._values_ori)), 2) - self.disp(A.color(A.BOLD, _(u'**SKIPPING**\n'))) - self.reset() - self.idx += 1 - raise exceptions.CancelError - - if not isinstance(value, list): - value = [value] - - for v in value: - if arg_type == OPT_STDIN: - self._stdin.append(v.encode('utf-8')) - elif arg_type == OPT_SHORT: - self._opts.append('-{}'.format(arg_name)) - self._opts.append(v.encode('utf-8')) - elif arg_type == OPT_LONG: - self._opts.append('--{}'.format(arg_name)) - self._opts.append(v.encode('utf-8')) - elif arg_type == OPT_POS: - self._pos.append(v.encode('utf-8')) - elif arg_type == OPT_IGNORE: - pass - else: - self.parser.error(_(u"Invalid argument, an option type is expected, got {type_}:{name}").format( - type_=arg_type, name=arg_name)) - - def runCommand(self): - """run requested command with parsed arguments""" - if self.args_idx != len(self.args.arguments): - self.disp(_(u"arguments in input data and in arguments sequence don't match"), error=True) - self.host.quit(C.EXIT_DATA_ERROR) - self.disp(A.color(C.A_HEADER, _(u'command {idx}').format(idx=self.idx)), no_lf=not self.args.debug) - stdin = ''.join(self._stdin) - if self.args.debug: - self.disp(A.color(C.A_SUBHEADER, _(u'values: '), A.RESET, u', '.join(self._values_ori)), 2) - - if stdin: - self.disp(A.color(C.A_SUBHEADER, u'--- STDIN ---')) - self.disp(stdin.decode('utf-8')) - self.disp(A.color(C.A_SUBHEADER, u'-------------')) - self.disp(u'{indent}{prog} {static} {options} {positionals}'.format( - indent = 4*u' ', - prog=sys.argv[0], - static = ' '.join(self.args.command).decode('utf-8'), - options = u' '.join([o.decode('utf-8') for o in self._opts]), - positionals = u' '.join([p.decode('utf-8') for p in self._pos]) - )) - self.disp(u'\n') - else: - self.disp(u' (' + u', '.join(self._values_ori) + u')', 2, no_lf=True) - args = [sys.argv[0]] + self.args.command + self._opts + self._pos - p = subprocess.Popen(args, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (stdout, stderr) = p.communicate(stdin) - log = self.args.log - log_err = self.args.log_err - log_tpl = '{command}\n{buff}\n\n' - if log: - log.write(log_tpl.format(command=' '.join(args), buff=stdout)) - if log_err: - log_err.write(log_tpl.format(command=' '.join(args), buff=stderr)) - ret = p.wait() - if ret == 0: - self.disp(A.color(C.A_SUCCESS, _(u'OK'))) - else: - self.disp(A.color(C.A_FAILURE, _(u'FAILED'))) - - self.reset() - self.idx += 1 - - def filter(self, filter_type, filter_arg, value): - """change input value - - @param filter_type(unicode): name of the filter - @param filter_arg(unicode, None): argument of the filter - @param value(unicode): value to filter - @return (unicode, False, None): modified value - False to skip the whole row - None to ignore this argument (but continue row with other ones) - """ - raise NotImplementedError - - -class Csv(InputCommon): - - def __init__(self, host): - super(Csv, self).__init__(host, 'csv', _(u'comma-separated values')) - - def add_parser_options(self): - InputCommon.add_parser_options(self) - self.parser.add_argument("-r", "--row", type=int, default=0, help=_(u"starting row (previous ones will be ignored)")) - self.parser.add_argument("-S", "--split", action='append_const', const=('split', None), dest='arguments', help=_(u"split value in several options")) - self.parser.add_argument("-E", "--empty", action='append', type=self.opt('empty'), dest='arguments', - help=_(u"action to do on empty value ({choices})").format(choices=u', '.join(OPT_EMPTY_CHOICES))) - - def filter(self, filter_type, filter_arg, value): - if filter_type == 'split': - return value.split() - elif filter_type == 'empty': - if filter_arg == OPT_EMPTY_IGNORE: - return value if value else None - elif filter_arg == OPT_EMPTY_SKIP: - return value if value else False - else: - self.parser.error(_(u"--empty value must be one of {choices}").format(choices=u', '.join(OPT_EMPTY_CHOICES))) - - super(Csv, self).filter(filter_type, filter_arg, value) - - def start(self): - import csv - reader = csv.reader(sys.stdin) - for idx, row in enumerate(reader): - try: - if idx < self.args.row: - continue - for value in row: - self.addValue(value.decode(self.args.encoding)) - self.runCommand() - except exceptions.CancelError: - # this row has been cancelled, we skip it - continue - - -class Input(base.CommandBase): - subcommands = (Csv,) - - def __init__(self, host): - super(Input, self).__init__(host, 'input', use_profile=False, help=_(u'launch command with external input'))
--- a/frontends/src/jp/cmd_invitation.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,219 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat.tools.common.ansi import ANSI as A -from sat.tools.common import data_format -from functools import partial - -__commands__ = ["Invitation"] - - -class Create(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'create', use_profile=False, use_output=C.OUTPUT_DICT, help=_(u'create and send an invitation')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-j", "--jid", type=base.unicode_decoder, default='', help='jid of the invitee (default: generate one)') - self.parser.add_argument("-P", "--password", type=base.unicode_decoder, default='', help='password of the invitee profile/XMPP account (default: generate one)') - self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default='', help='name of the invitee') - self.parser.add_argument("-N", "--host-name", type=base.unicode_decoder, default='', help='name of the host') - self.parser.add_argument("-e", "--email", action="append", type=base.unicode_decoder, default=[], help='email(s) to send the invitation to (if --no-email is set, email will just be saved)') - self.parser.add_argument("--no-email", action="store_true", help='do NOT send invitation email') - self.parser.add_argument("-l", "--lang", type=base.unicode_decoder, default='', help='main language spoken by the invitee') - self.parser.add_argument("-u", "--url", type=base.unicode_decoder, default='', help='template to construct the URL') - self.parser.add_argument("-s", "--subject", type=base.unicode_decoder, default='', help='subject of the invitation email (default: generic subject)') - self.parser.add_argument("-b", "--body", type=base.unicode_decoder, default='', help='body of the invitation email (default: generic body)') - self.parser.add_argument("-x", "--extra", metavar=('KEY', 'VALUE'), type=base.unicode_decoder, action='append', nargs=2, default=[], help='extra data to associate with invitation/invitee') - self.parser.add_argument("-p", "--profile", type=base.unicode_decoder, default='', help="profile doing the invitation (default: don't associate profile)") - - def invitationCreateCb(self, invitation_data): - self.output(invitation_data) - self.host.quit(C.EXIT_OK) - - def invitationCreateEb(self, failure_): - self.disp(u"can't create invitation: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - extra = dict(self.args.extra) - email = self.args.email[0] if self.args.email else None - emails_extra = self.args.email[1:] - if self.args.no_email: - if email: - extra['email'] = email - data_format.iter2dict(u'emails_extra', emails_extra) - else: - if not email: - self.parser.error(_(u'you need to specify an email address to send email invitation')) - - self.host.bridge.invitationCreate( - email, - emails_extra, - self.args.jid, - self.args.password, - self.args.name, - self.args.host_name, - self.args.lang, - self.args.url, - self.args.subject, - self.args.body, - extra, - self.args.profile, - callback=self.invitationCreateCb, - errback=self.invitationCreateEb) - - -class Get(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'get', use_profile=False, use_output=C.OUTPUT_DICT, help=_(u'get invitation data')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("id", type=base.unicode_decoder, - help=_(u"invitation UUID")) - self.parser.add_argument("-j", "--with-jid", action="store_true", help=_(u"start profile session and retrieve jid")) - - def output_data(self, data, jid_=None): - if jid_ is not None: - data['jid'] = jid_ - self.output(data) - self.host.quit() - - def invitationGetCb(self, invitation_data): - if self.args.with_jid: - profile = invitation_data[u'guest_profile'] - def session_started(dummy): - self.host.bridge.asyncGetParamA( - u'JabberID', - u'Connection', - profile_key=profile, - callback=lambda jid_: self.output_data(invitation_data, jid_), - errback=partial(self.errback, - msg=_(u"can't retrieve jid: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - self.host.bridge.profileStartSession( - invitation_data[u'password'], - profile, - callback=session_started, - errback=partial(self.errback, - msg=_(u"can't start session: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - else: - self.output_data(invitation_data) - - def start(self): - self.host.bridge.invitationGet( - self.args.id, - callback=self.invitationGetCb, - errback=partial(self.errback, - msg=_(u"can't get invitation data: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Modify(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'modify', use_profile=False, help=_(u'modify existing invitation')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("--replace", action='store_true', help='replace the whole data') - self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default='', help='name of the invitee') - self.parser.add_argument("-N", "--host-name", type=base.unicode_decoder, default='', help='name of the host') - self.parser.add_argument("-e", "--email", type=base.unicode_decoder, default='', help='email to send the invitation to (if --no-email is set, email will just be saved)') - self.parser.add_argument("-l", "--lang", dest="language", type=base.unicode_decoder, default='', - help='main language spoken by the invitee') - self.parser.add_argument("-x", "--extra", metavar=('KEY', 'VALUE'), type=base.unicode_decoder, action='append', nargs=2, default=[], help='extra data to associate with invitation/invitee') - self.parser.add_argument("-p", "--profile", type=base.unicode_decoder, default='', help="profile doing the invitation (default: don't associate profile") - self.parser.add_argument("id", type=base.unicode_decoder, - help=_(u"invitation UUID")) - - def invitationModifyCb(self): - self.disp(_(u'invitations have been modified correctly')) - self.host.quit(C.EXIT_OK) - - def invitationModifyEb(self, failure_): - self.disp(u"can't create invitation: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - extra = dict(self.args.extra) - for arg_name in ('name', 'host_name', 'email', 'language', 'profile'): - value = getattr(self.args, arg_name) - if not value: - continue - if arg_name in extra: - self.parser.error(_(u"you can't set {arg_name} in both optional argument and extra").format(arg_name=arg_name)) - extra[arg_name] = value - self.host.bridge.invitationModify( - self.args.id, - extra, - self.args.replace, - callback=self.invitationModifyCb, - errback=self.invitationModifyEb) - - -class List(base.CommandBase): - - def __init__(self, host): - extra_outputs = {'default': self.default_output} - base.CommandBase.__init__(self, host, 'list', use_profile=False, use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'list invitations data')) - self.need_loop=True - - def default_output(self, data): - for idx, datum in enumerate(data.iteritems()): - if idx: - self.disp(u"\n") - key, invitation_data = datum - self.disp(A.color(C.A_HEADER, key)) - indent = u' ' - for k, v in invitation_data.iteritems(): - self.disp(indent + A.color(C.A_SUBHEADER, k + u':') + u' ' + unicode(v)) - - def add_parser_options(self): - self.parser.add_argument("-p", "--profile", default=C.PROF_KEY_NONE, help=_(u"return only invitations linked to this profile")) - - def invitationListCb(self, data): - self.output(data) - self.host.quit() - - def start(self): - self.host.bridge.invitationList( - self.args.profile, - callback=self.invitationListCb, - errback=partial(self.errback, - msg=_(u"can't list invitations: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Invitation(base.CommandBase): - subcommands = (Create, Get, Modify, List) - - def __init__(self, host): - super(Invitation, self).__init__(host, 'invitation', use_profile=False, help=_(u'invitation of user(s) without XMPP account'))
--- a/frontends/src/jp/cmd_merge_request.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import xmlui_manager -from sat_frontends.jp import common -from functools import partial -import os.path - -__commands__ = ["MergeRequest"] - - -class Set(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, - pubsub_defaults = {u'service': _(u'auto'), u'node': _(u'auto')}, - help=_(u'publish or update a merge request')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-i", "--item", type=base.unicode_decoder, default=u'', help=_(u"id or URL of the request to update, or nothing for a new one")) - self.parser.add_argument("-r", "--repository", metavar="PATH", type=base.unicode_decoder, default=u'.', help=_(u"path of the repository (DEFAULT: current directory)")) - self.parser.add_argument("-f", "--force", action="store_true", help=_(u"publish merge request without confirmation")) - self.parser.add_argument("-l", "--label", dest="labels", type=base.unicode_decoder, action='append', help=_(u"labels to categorize your request")) - - def mergeRequestSetCb(self, published_id): - if published_id: - self.disp(u"Merge request published at {pub_id}".format(pub_id=published_id)) - else: - self.disp(u"Merge request published") - self.host.quit(C.EXIT_OK) - - def sendRequest(self): - extra = {'update': 'true'} if self.args.item else {} - values = {} - if self.args.labels is not None: - values[u'labels'] = self.args.labels - self.host.bridge.mergeRequestSet( - self.args.service, - self.args.node, - self.repository, - u'auto', - values, - u'', - self.args.item, - extra, - self.profile, - callback=self.mergeRequestSetCb, - errback=partial(self.errback, - msg=_(u"can't create merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def askConfirmation(self): - if not self.args.force: - message = _(u"You are going to publish your changes to service [{service}], are you sure ?").format( - service=self.args.service) - self.host.confirmOrQuit(message, _(u"merge request publication cancelled")) - self.sendRequest() - - def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - common.URIFinder(self, self.repository, 'merge requests', self.askConfirmation) - - -class Get(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'get', use_verbose=True, - use_pubsub=True, pubsub_flags={C.MULTI_ITEMS}, - pubsub_defaults = {u'service': _(u'auto'), u'node': _(u'auto')}, - help=_(u'get a merge request')) - self.need_loop=True - - def add_parser_options(self): - pass - - def mergeRequestGetCb(self, requests_data): - if self.verbosity >= 1: - whitelist = None - else: - whitelist = {'id', 'title', 'body'} - for request_xmlui in requests_data[0]: - xmlui = xmlui_manager.create(self.host, request_xmlui, whitelist=whitelist) - xmlui.show(values_only=True) - self.disp(u'') - self.host.quit(C.EXIT_OK) - - def getRequests(self): - extra = {} - self.host.bridge.mergeRequestsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - u'', - extra, - self.profile, - callback=self.mergeRequestGetCb, - errback=partial(self.errback, - msg=_(u"can't get merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def start(self): - common.URIFinder(self, os.getcwd(), 'merge requests', self.getRequests, meta_map={}) - - -class Import(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'import', - use_pubsub=True, pubsub_flags={C.SINGLE_ITEM, C.ITEM}, - pubsub_defaults = {u'service': _(u'auto'), u'node': _(u'auto')}, - help=_(u'import a merge request')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-r", "--repository", metavar="PATH", type=base.unicode_decoder, default=u'.', help=_(u"path of the repository (DEFAULT: current directory)")) - - def mergeRequestImportCb(self): - self.host.quit(C.EXIT_OK) - - def importRequest(self): - extra = {} - self.host.bridge.mergeRequestsImport( - self.repository, - self.args.item, - self.args.service, - self.args.node, - extra, - self.profile, - callback=self.mergeRequestImportCb, - errback=partial(self.errback, - msg=_(u"can't import merge request: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def start(self): - self.repository = os.path.expanduser(os.path.abspath(self.args.repository)) - common.URIFinder(self, self.repository, 'merge requests', self.importRequest, meta_map={}) - - -class MergeRequest(base.CommandBase): - subcommands = (Set, Get, Import) - - def __init__(self, host): - super(MergeRequest, self).__init__(host, 'merge-request', use_profile=False, help=_('merge-request management'))
--- a/frontends/src/jp/cmd_message.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.jp import base -import sys -from sat.core.i18n import _ -from sat.core.constants import Const as C -from sat.tools.utils import clean_ustr - -__commands__ = ["Message"] - - -class Send(base.CommandBase): - - def __init__(self, host): - super(Send, self).__init__(host, 'send', help=_('send a message to a contact')) - - def add_parser_options(self): - self.parser.add_argument("-l", "--lang", type=str, default='', help=_(u"language of the message")) - self.parser.add_argument("-s", "--separate", action="store_true", help=_(u"separate xmpp messages: send one message per line instead of one message alone.")) - self.parser.add_argument("-n", "--new-line", action="store_true", help=_(u"add a new line at the beginning of the input (usefull for ascii art ;))")) - self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, help=_(u"subject of the message")) - self.parser.add_argument("-L", "--subject_lang", type=str, default='', help=_(u"language of subject")) - self.parser.add_argument("-t", "--type", choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,), default=C.MESS_TYPE_AUTO, help=_("type of the message")) - syntax = self.parser.add_mutually_exclusive_group() - syntax.add_argument("-x", "--xhtml", action="store_true", help=_(u"XHTML body")) - syntax.add_argument("-r", "--rich", action="store_true", help=_(u"rich body")) - self.parser.add_argument("jid", type=base.unicode_decoder, help=_(u"the destination jid")) - - def start(self): - if self.args.xhtml and self.args.separate: - self.disp(u"argument -s/--separate is not compatible yet with argument -x/--xhtml", error=True) - self.host.quit(2) - - jids = self.host.check_jids([self.args.jid]) - jid = jids[0] - self.sendStdin(jid) - - def sendStdin(self, dest_jid): - """Send incomming data on stdin to jabber contact - - @param dest_jid: destination jid - """ - header = "\n" if self.args.new_line else "" - stdin_lines = [stream.decode('utf-8','ignore') for stream in sys.stdin.readlines()] - extra = {} - if self.args.subject is None: - subject = {} - else: - subject = {self.args.subject_lang: self.args.subject} - - if self.args.xhtml or self.args.rich: - key = u"xhtml" if self.args.xhtml else u"rich" - if self.args.lang: - key = u"{}_{}".format(key, self.args.lang) - extra[key] = clean_ustr(u"".join(stdin_lines)) - stdin_lines = [] - - if self.args.separate: #we send stdin in several messages - if header: - self.host.bridge.messageSend(dest_jid, {self.args.lang: header}, subject, self.args.type, profile_key=self.profile, callback=lambda: None, errback=lambda ignore: ignore) - - for line in stdin_lines: - self.host.bridge.messageSend(dest_jid, {self.args.lang: line.replace("\n","")}, subject, self.args.type, extra, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore) - - else: - msg = {self.args.lang: header + clean_ustr(u"".join(stdin_lines))} if not (self.args.xhtml or self.args.rich) else {} - self.host.bridge.messageSend(dest_jid, msg, subject, self.args.type, extra, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore) - - -class Message(base.CommandBase): - subcommands = (Send, ) - - def __init__(self, host): - super(Message, self).__init__(host, 'message', use_profile=False, help=_('messages handling'))
--- a/frontends/src/jp/cmd_param.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -__commands__ = ["Param"] - - -class Get(base.CommandBase): - def __init__(self, host): - super(Get, self).__init__(host, 'get', need_connect=False, help=_('Get a parameter value')) - - def add_parser_options(self): - self.parser.add_argument("category", nargs='?', type=base.unicode_decoder, help=_(u"Category of the parameter")) - self.parser.add_argument("name", nargs='?', type=base.unicode_decoder, help=_(u"Name of the parameter")) - self.parser.add_argument("-a", "--attribute", type=str, default="value", help=_(u"Name of the attribute to get")) - self.parser.add_argument("--security-limit", type=int, default=-1, help=_(u"Security limit")) - - def start(self): - if self.args.category is None: - categories = self.host.bridge.getParamsCategories() - print u"\n".join(categories) - elif self.args.name is None: - try: - values_dict = self.host.bridge.asyncGetParamsValuesFromCategory(self.args.category, self.args.security_limit, self.profile) - except Exception as e: - print u"Can't find requested parameters: {}".format(e) - self.host.quit(1) - for name, value in values_dict.iteritems(): - print u"{}\t{}".format(name, value) - else: - try: - value = self.host.bridge.asyncGetParamA(self.args.name, self.args.category, self.args.attribute, - self.args.security_limit, self.profile) - except Exception as e: - print u"Can't find requested parameter: {}".format(e) - self.host.quit(1) - print value - - -class Set(base.CommandBase): - def __init__(self, host): - super(Set, self).__init__(host, 'set', need_connect=False, help=_('Set a parameter value')) - - def add_parser_options(self): - self.parser.add_argument("category", type=base.unicode_decoder, help=_(u"Category of the parameter")) - self.parser.add_argument("name", type=base.unicode_decoder, help=_(u"Name of the parameter")) - self.parser.add_argument("value", type=base.unicode_decoder, help=_(u"Name of the parameter")) - self.parser.add_argument("--security-limit", type=int, default=-1, help=_(u"Security limit")) - - def start(self): - try: - self.host.bridge.setParam(self.args.name, self.args.value, self.args.category, self.args.security_limit, self.profile) - except Exception as e: - print u"Can set requested parameter: {}".format(e) - - -class SaveTemplate(base.CommandBase): - def __init__(self, host): - super(SaveTemplate, self).__init__(host, 'save', use_profile=False, help=_('Save parameters template to xml file')) - - def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("Output file")) - - def start(self): - """Save parameters template to xml file""" - if self.host.bridge.saveParamsTemplate(self.args.filename): - print _("Parameters saved to file %s") % self.args.filename - else: - print _("Can't save parameters to file %s") % self.args.filename - - -class LoadTemplate(base.CommandBase): - - def __init__(self, host): - super(LoadTemplate, self).__init__(host, 'load', use_profile=False, help=_('Load parameters template from xml file')) - - def add_parser_options(self): - self.parser.add_argument("filename", type=str, help=_("Input file")) - - def start(self): - """Load parameters template from xml file""" - if self.host.bridge.loadParamsTemplate(self.args.filename): - print _("Parameters loaded from file %s") % self.args.filename - else: - print _("Can't load parameters from file %s") % self.args.filename - - -class Param(base.CommandBase): - subcommands = (Get, Set, SaveTemplate, LoadTemplate) - - def __init__(self, host): - super(Param, self).__init__(host, 'param', use_profile=False, help=_('Save/load parameters template'))
--- a/frontends/src/jp/cmd_pipe.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sat_frontends.jp import base - -from sat_frontends.jp.constants import Const as C -import sys -from sat.core.i18n import _ -from sat_frontends.tools import jid -import xml.etree.ElementTree as ET # FIXME: used temporarily to manage XMLUI -from functools import partial -import socket -import SocketServer -import errno - -__commands__ = ["Pipe"] - -START_PORT = 9999 - -class PipeOut(base.CommandBase): - - def __init__(self, host): - super(PipeOut, self).__init__(host, 'out', help=_('send a pipe a stream')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument("jid", type=base.unicode_decoder, help=_("the destination jid")) - - def streamOutCb(self, port): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(('127.0.0.1', int(port))) - while True: - buf = sys.stdin.read(4096) - if not buf: - break - try: - s.sendall(buf) - except socket.error as e: - if e.errno == errno.EPIPE: - sys.stderr.write(str(e) + '\n') - self.host.quit(1) - else: - raise e - self.host.quit() - - def start(self): - """ Create named pipe, and send stdin to it """ - self.host.bridge.streamOut( - self.host.get_full_jid(self.args.jid), - self.profile, - callback=self.streamOutCb, - errback=partial(self.errback, - msg=_(u"can't start stream: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class StreamServer(SocketServer.BaseRequestHandler): - - def handle(self): - while True: - data = self.request.recv(4096) - if not data: - break - sys.stdout.write(data) - try: - sys.stdout.flush() - except IOError as e: - sys.stderr.write(str(e) + '\n') - break - # calling shutdown will do a deadlock as we don't use separate thread - # this is a workaround (cf. https://stackoverflow.com/a/36017741) - self.server._BaseServer__shutdown_request = True - - -class PipeIn(base.CommandAnswering): - - def __init__(self, host): - super(PipeIn, self).__init__(host, 'in', help=_('receive a pipe stream')) - self.action_callbacks = {"STREAM": self.onStreamAction} - - def add_parser_options(self): - self.parser.add_argument("jids", type=base.unicode_decoder, nargs="*", help=_('Jids accepted (none means "accept everything")')) - - def getXmluiId(self, action_data): - # FIXME: we temporarily use ElementTree, but a real XMLUI managing module - # should be available in the future - # TODO: XMLUI module - try: - xml_ui = action_data['xmlui'] - except KeyError: - self.disp(_(u"Action has no XMLUI"), 1) - else: - ui = ET.fromstring(xml_ui.encode('utf-8')) - xmlui_id = ui.get('submit') - if not xmlui_id: - self.disp(_(u"Invalid XMLUI received"), error=True) - return xmlui_id - - def onStreamAction(self, action_data, action_id, security_limit, profile): - xmlui_id = self.getXmluiId(action_data) - if xmlui_id is None: - return self.host.quitFromSignal(1) - try: - from_jid = jid.JID(action_data['meta_from_jid']) - except KeyError: - self.disp(_(u"Ignoring action without from_jid data"), 1) - return - - if not self.bare_jids or from_jid.bare in self.bare_jids: - host, port = "localhost", START_PORT - while True: - try: - server = SocketServer.TCPServer((host, port), StreamServer) - except socket.error as e: - if e.errno == errno.EADDRINUSE: - port += 1 - else: - raise e - else: - break - xmlui_data = {'answer': C.BOOL_TRUE, - 'port': unicode(port)} - self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile) - server.serve_forever() - self.host.quitFromSignal() - - def start(self): - self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids] - - -class Pipe(base.CommandBase): - subcommands = (PipeOut, PipeIn) - - def __init__(self, host): - super(Pipe, self).__init__(host, 'pipe', use_profile=False, help=_('stream piping through XMPP'))
--- a/frontends/src/jp/cmd_profile.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,209 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -"""This module permits to manage profiles. It can list, create, delete -and retrieve information about a profile.""" - -from sat_frontends.jp.constants import Const as C -from sat.core.log import getLogger -log = getLogger(__name__) -from sat.core.i18n import _ -from sat_frontends.jp import base -from functools import partial - -__commands__ = ["Profile"] - -PROFILE_HELP = _('The name of the profile') - - -class ProfileConnect(base.CommandBase): - """Dummy command to use profile_session parent, i.e. to be able to connect without doing anything else""" - - def __init__(self, host): - # it's weird to have a command named "connect" with need_connect=False, but it can be handy to be able - # to launch just the session, so some paradoxes don't hurt - super(ProfileConnect, self).__init__(host, 'connect', need_connect=False, help=(u'connect a profile')) - - def add_parser_options(self): - pass - - -class ProfileDisconnect(base.CommandBase): - - def __init__(self, host): - super(ProfileDisconnect, self).__init__(host, 'disconnect', need_connect=False, help=(u'disconnect a profile')) - self.need_loop = True - - def add_parser_options(self): - pass - - def start(self): - self.host.bridge.disconnect(self.args.profile, callback=self.host.quit) - - -class ProfileDefault(base.CommandBase): - def __init__(self, host): - super(ProfileDefault, self).__init__(host, 'default', use_profile=False, help=(u'print default profile')) - - def add_parser_options(self): - pass - - def start(self): - print self.host.bridge.profileNameGet('@DEFAULT@') - - -class ProfileDelete(base.CommandBase): - def __init__(self, host): - super(ProfileDelete, self).__init__(host, 'delete', use_profile=False, help=(u'delete a profile')) - - def add_parser_options(self): - self.parser.add_argument('profile', type=str, help=PROFILE_HELP) - self.parser.add_argument('-f', '--force', action='store_true', help=_(u'delete profile without confirmation')) - - def start(self): - if self.args.profile not in self.host.bridge.profilesListGet(): - log.error("Profile %s doesn't exist." % self.args.profile) - self.host.quit(1) - if not self.args.force: - message = u"Are you sure to delete profile [{}] ?".format(self.args.profile) - res = raw_input("{} (y/N)? ".format(message)) - if res not in ("y", "Y"): - self.disp(_(u"Profile deletion cancelled")) - self.host.quit(2) - - self.host.bridge.asyncDeleteProfile(self.args.profile, callback=lambda dummy: None) - - -class ProfileInfo(base.CommandBase): - def __init__(self, host): - super(ProfileInfo, self).__init__(host, 'info', need_connect=False, help=_(u'get information about a profile')) - self.need_loop = True - self.to_show = [(_(u"jid"), "Connection", "JabberID"),] - self.largest = max([len(item[0]) for item in self.to_show]) - - - def add_parser_options(self): - self.parser.add_argument('--show-password', action='store_true', help=_(u'show the XMPP password IN CLEAR TEXT')) - - def showNextValue(self, label=None, category=None, value=None): - """Show next value from self.to_show and quit on last one""" - if label is not None: - print((u"{label:<"+unicode(self.largest+2)+"}{value}").format(label=label+": ", value=value)) - try: - label, category, name = self.to_show.pop(0) - except IndexError: - self.host.quit() - else: - self.host.bridge.asyncGetParamA(name, category, profile_key=self.host.profile, - callback=lambda value: self.showNextValue(label, category, value)) - - def start(self): - if self.args.show_password: - self.to_show.append((_(u"XMPP password"), "Connection", "Password")) - self.showNextValue() - - -class ProfileList(base.CommandBase): - def __init__(self, host): - super(ProfileList, self).__init__(host, 'list', use_profile=False, use_output='list', help=(u'list profiles')) - - def add_parser_options(self): - group = self.parser.add_mutually_exclusive_group() - group.add_argument('-c', '--clients', action='store_true', help=_(u'get clients profiles only')) - group.add_argument('-C', '--components', action='store_true', help=(u'get components profiles only')) - - - def start(self): - if self.args.clients: - clients, components = True, False - elif self.args.components: - clients, components = False, True - else: - clients, components = True, True - self.output(self.host.bridge.profilesListGet(clients, components)) - - -class ProfileCreate(base.CommandBase): - def __init__(self, host): - super(ProfileCreate, self).__init__(host, 'create', use_profile=False, help=(u'create a new profile')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('profile', type=str, help=_(u'the name of the profile')) - self.parser.add_argument('-p', '--password', type=str, default='', help=_(u'the password of the profile')) - self.parser.add_argument('-j', '--jid', type=str, help=_(u'the jid of the profile')) - self.parser.add_argument('-x', '--xmpp-password', type=str, help=_(u'the password of the XMPP account (use profile password if not specified)'), - metavar='PASSWORD') - self.parser.add_argument('-C', '--component', type=base.unicode_decoder, default='', - help=_(u'set to component import name (entry point) if this is a component')) - - def _session_started(self, dummy): - if self.args.jid: - self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.args.profile) - xmpp_pwd = self.args.password or self.args.xmpp_password - if xmpp_pwd: - self.host.bridge.setParam("Password", xmpp_pwd, "Connection", profile_key=self.args.profile) - self.host.quit() - - def _profile_created(self): - self.host.bridge.profileStartSession(self.args.password, self.args.profile, callback=self._session_started, errback=None) - - def start(self): - """Create a new profile""" - if self.args.profile in self.host.bridge.profilesListGet(): - log.error("Profile %s already exists." % self.args.profile) - self.host.quit(1) - self.host.bridge.profileCreate(self.args.profile, self.args.password, self.args.component, - callback=self._profile_created, - errback=partial(self.errback, - msg=_(u"can't create profile: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class ProfileModify(base.CommandBase): - def __init__(self, host): - super(ProfileModify, self).__init__(host, 'modify', need_connect=False, help=_(u'modify an existing profile')) - - def add_parser_options(self): - profile_pwd_group = self.parser.add_mutually_exclusive_group() - profile_pwd_group.add_argument('-w', '--password', type=base.unicode_decoder, help=_(u'change the password of the profile')) - profile_pwd_group.add_argument('--disable-password', action='store_true', help=_(u'disable profile password (dangerous!)')) - self.parser.add_argument('-j', '--jid', type=base.unicode_decoder, help=_(u'the jid of the profile')) - self.parser.add_argument('-x', '--xmpp-password', type=base.unicode_decoder, help=_(u'change the password of the XMPP account'), - metavar='PASSWORD') - self.parser.add_argument('-D', '--default', action='store_true', help=_(u'set as default profile')) - - def start(self): - if self.args.disable_password: - self.args.password = '' - if self.args.password is not None: - self.host.bridge.setParam("Password", self.args.password, "General", profile_key=self.host.profile) - if self.args.jid is not None: - self.host.bridge.setParam("JabberID", self.args.jid, "Connection", profile_key=self.host.profile) - if self.args.xmpp_password is not None: - self.host.bridge.setParam("Password", self.args.xmpp_password, "Connection", profile_key=self.host.profile) - if self.args.default: - self.host.bridge.profileSetDefault(self.host.profile) - - -class Profile(base.CommandBase): - subcommands = (ProfileConnect, ProfileDisconnect, ProfileCreate, ProfileDefault, ProfileDelete, ProfileInfo, ProfileList, ProfileModify) - - def __init__(self, host): - super(Profile, self).__init__(host, 'profile', use_profile=False, help=_(u'profile commands'))
--- a/frontends/src/jp/cmd_pubsub.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1198 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -from sat.core.i18n import _ -from sat.core import exceptions -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import common -from sat_frontends.jp import arg_tools -from functools import partial -from sat.tools.common import uri -from sat.tools.common.ansi import ANSI as A -from sat_frontends.tools import jid, strings -import argparse -import os.path -import re -import subprocess -import sys - -__commands__ = ["Pubsub"] - -PUBSUB_TMP_DIR = u"pubsub" -PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema" -ALLOWED_SUBSCRIPTIONS_OWNER = ('subscribed', 'pending', 'none') - -# TODO: need to split this class in several modules, plugin should handle subcommands - - -class NodeInfo(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'info', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node configuration')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys', - help=_(u"data key to filter")) - - def removePrefix(self, key): - return key[7:] if key.startswith(u"pubsub#") else key - - def filterKey(self, key): - return any((key == k or key == u'pubsub#' + k) for k in self.args.keys) - - def psNodeConfigurationGetCb(self, config_dict): - key_filter = (lambda k: True) if not self.args.keys else self.filterKey - config_dict = {self.removePrefix(k):v for k,v in config_dict.iteritems() if key_filter(k)} - self.output(config_dict) - self.host.quit() - - def psNodeConfigurationGetEb(self, failure_): - self.disp(u"can't get node configuration: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeConfigurationGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeConfigurationGetCb, - errback=self.psNodeConfigurationGetEb) - - -class NodeCreate(base.CommandBase): - - def __init__(self, host): - 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')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields', - default=[], metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set")) - self.parser.add_argument("-F", "--full-prefix", action="store_true", help=_(u"don't prepend \"pubsub#\" prefix to field names")) - - def psNodeCreateCb(self, node_id): - if self.host.verbosity: - announce = _(u'node created successfully: ') - else: - announce = u'' - self.disp(announce + node_id) - self.host.quit() - - def psNodeCreateEb(self, failure_): - self.disp(u"can't create: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - if not self.args.full_prefix: - options = {u'pubsub#' + k: v for k,v in self.args.fields} - else: - options = dict(self.args.fields) - self.host.bridge.psNodeCreate( - self.args.service, - self.args.node, - options, - self.profile, - callback=self.psNodeCreateCb, - errback=partial(self.errback, - msg=_(u"can't create node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class NodeDelete(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a node')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument('-f', '--force', action='store_true', help=_(u'delete node without confirmation')) - - def psNodeDeleteCb(self): - self.disp(_(u'node [{node}] deleted successfully').format(node=self.args.node)) - self.host.quit() - - def start(self): - if not self.args.force: - if not self.args.service: - message = _(u"Are you sure to delete pep node [{node_id}] ?").format( - node_id=self.args.node) - else: - message = _(u"Are you sure to delete node [{node_id}] on service [{service}] ?").format( - node_id=self.args.node, service=self.args.service) - self.host.confirmOrQuit(message, _(u"node deletion cancelled")) - - self.host.bridge.psNodeDelete( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeDeleteCb, - errback=partial(self.errback, - msg=_(u"can't delete node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class NodeSet(base.CommandBase): - - def __init__(self, host): - 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')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields', - required=True, metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set (required)")) - - def psNodeConfigurationSetCb(self): - self.disp(_(u'node configuration successful'), 1) - self.host.quit() - - def psNodeConfigurationSetEb(self, failure_): - self.disp(u"can't set node configuration: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def getKeyName(self, k): - if not k.startswith(u'pubsub#'): - return u'pubsub#' + k - else: - return k - - def start(self): - self.host.bridge.psNodeConfigurationSet( - self.args.service, - self.args.node, - {self.getKeyName(k): v for k,v in self.args.fields}, - self.profile, - callback=self.psNodeConfigurationSetCb, - errback=self.psNodeConfigurationSetEb) - - -class NodeAffiliationsGet(base.CommandBase): - - def __init__(self, host): - 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)')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psNodeAffiliationsGetCb(self, affiliations): - self.output(affiliations) - self.host.quit() - - def psNodeAffiliationsGetEb(self, failure_): - self.disp(u"can't get node affiliations: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeAffiliationsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeAffiliationsGetCb, - errback=self.psNodeAffiliationsGetEb) - - -class NodeAffiliationsSet(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set affiliations (for node owner)')) - self.need_loop=True - - def add_parser_options(self): - # XXX: we use optional argument syntax for a required one because list of list of 2 elements - # (uses to construct dicts) don't work with positional arguments - self.parser.add_argument("-a", - "--affiliation", - dest="affiliations", - metavar=('JID', 'AFFILIATION'), - required=True, - type=base.unicode_decoder, - action="append", - nargs=2, - help=_(u"entity/affiliation couple(s)")) - - def psNodeAffiliationsSetCb(self): - self.disp(_(u"affiliations have been set"), 1) - self.host.quit() - - def psNodeAffiliationsSetEb(self, failure_): - self.disp(u"can't set node affiliations: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - affiliations = dict(self.args.affiliations) - self.host.bridge.psNodeAffiliationsSet( - self.args.service, - self.args.node, - affiliations, - self.profile, - callback=self.psNodeAffiliationsSetCb, - errback=self.psNodeAffiliationsSetEb) - - -class NodeAffiliations(base.CommandBase): - subcommands = (NodeAffiliationsGet, NodeAffiliationsSet) - - def __init__(self, host): - super(NodeAffiliations, self).__init__(host, 'affiliations', use_profile=False, help=_(u'set or retrieve node affiliations')) - - -class NodeSubscriptionsGet(base.CommandBase): - - def __init__(self, host): - 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)')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psNodeSubscriptionsGetCb(self, subscriptions): - self.output(subscriptions) - self.host.quit() - - def psNodeSubscriptionsGetEb(self, failure_): - self.disp(u"can't get node subscriptions: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeSubscriptionsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psNodeSubscriptionsGetCb, - errback=self.psNodeSubscriptionsGetEb) - - -class StoreSubscriptionAction(argparse.Action): - """Action which handle subscription parameter for owner - - list is given by pairs: jid and subscription state - if subscription state is not specified, it default to "subscribed" - """ - - def __call__(self, parser, namespace, values, option_string): - dest_dict = getattr(namespace, self.dest) - while values: - jid_s = values.pop(0) - try: - subscription = values.pop(0) - except IndexError: - subscription = 'subscribed' - if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER: - parser.error(_(u"subscription must be one of {}").format(u', '.join(ALLOWED_SUBSCRIPTIONS_OWNER))) - dest_dict[jid_s] = subscription - - -class NodeSubscriptionsSet(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/modify subscriptions (for node owner)')) - self.need_loop=True - - def add_parser_options(self): - # XXX: we use optional argument syntax for a required one because list of list of 2 elements - # (uses to construct dicts) don't work with positional arguments - self.parser.add_argument("-S", - "--subscription", - dest="subscriptions", - default={}, - nargs='+', - metavar=('JID [SUSBSCRIPTION]'), - required=True, - type=base.unicode_decoder, - action=StoreSubscriptionAction, - help=_(u"entity/subscription couple(s)")) - - def psNodeSubscriptionsSetCb(self): - self.disp(_(u"subscriptions have been set"), 1) - self.host.quit() - - def psNodeSubscriptionsSetEb(self, failure_): - self.disp(u"can't set node subscriptions: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psNodeSubscriptionsSet( - self.args.service, - self.args.node, - self.args.subscriptions, - self.profile, - callback=self.psNodeSubscriptionsSetCb, - errback=self.psNodeSubscriptionsSetEb) - - -class NodeSubscriptions(base.CommandBase): - subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet) - - def __init__(self, host): - super(NodeSubscriptions, self).__init__(host, 'subscriptions', use_profile=False, help=_(u'get or modify node subscriptions')) - - -class NodeSchemaSet(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/replace a schema')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument('schema', help=_(u"schema to set (must be XML)")) - - def psSchemaSetCb(self): - self.disp(_(u'schema has been set'), 1) - self.host.quit() - - def start(self): - self.host.bridge.psSchemaSet( - self.args.service, - self.args.node, - self.args.schema, - self.profile, - callback=self.psSchemaSetCb, - errback=partial(self.errback, - msg=_(u"can't set schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class NodeSchemaEdit(base.CommandBase, common.BaseEdit): - use_items=False - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.NODE}, use_draft=True, use_verbose=True, help=_(u'edit a schema')) - common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR) - self.need_loop=True - - def add_parser_options(self): - pass - - def psSchemaSetCb(self): - self.disp(_(u'schema has been set'), 1) - self.host.quit() - - def publish(self, schema): - self.host.bridge.psSchemaSet( - self.args.service, - self.args.node, - schema, - self.profile, - callback=self.psSchemaSetCb, - errback=partial(self.errback, - msg=_(u"can't set schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - def psSchemaGetCb(self, schema): - try: - from lxml import etree - except ImportError: - self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True) - self.host.quit(1) - content_file_obj, content_file_path = self.getTmpFile() - schema = schema.strip() - if schema: - parser = etree.XMLParser(remove_blank_text=True) - schema_elt = etree.fromstring(schema, parser) - content_file_obj.write(etree.tostring(schema_elt, encoding="utf-8", pretty_print=True)) - content_file_obj.seek(0) - self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj) - - def start(self): - self.host.bridge.psSchemaGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSchemaGetCb, - errback=partial(self.errback, - msg=_(u"can't edit schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class NodeSchemaGet(base.CommandBase): - - def __init__(self, host): - 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')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psSchemaGetCb(self, schema): - if not schema: - self.disp(_(u'no schema found'), 1) - self.host.quit(1) - self.output(schema) - self.host.quit() - - def start(self): - self.host.bridge.psSchemaGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSchemaGetCb, - errback=partial(self.errback, - msg=_(u"can't get schema: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class NodeSchema(base.CommandBase): - subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet) - - def __init__(self, host): - super(NodeSchema, self).__init__(host, 'schema', use_profile=False, help=_(u"data schema manipulation")) - - -class Node(base.CommandBase): - subcommands = (NodeInfo, NodeCreate, NodeDelete, NodeSet, NodeAffiliations, NodeSubscriptions, NodeSchema) - - def __init__(self, host): - super(Node, self).__init__(host, 'node', use_profile=False, help=_('node handling')) - - -class Set(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'publish a new item or update an existing one')) - self.need_loop=True - - def add_parser_options(self): - 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")) - - def psItemsSendCb(self, published_id): - if published_id: - self.disp(u"Item published at {pub_id}".format(pub_id=published_id)) - else: - self.disp(u"Item published") - self.host.quit(C.EXIT_OK) - - def start(self): - try: - from lxml import etree - except ImportError: - self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True) - self.host.quit(1) - try: - element = etree.parse(sys.stdin).getroot() - except Exception as e: - self.parser.error(_(u"Can't parse the payload XML in input: {msg}").format(msg=e)) - if element.tag in ('item', '{http://jabber.org/protocol/pubsub}item'): - if len(element) > 1: - self.parser.error(_(u"<item> can only have one child element (the payload)")) - element = element[0] - payload = etree.tostring(element, encoding='unicode') - - self.host.bridge.psItemSend(self.args.service, - self.args.node, - payload, - self.args.item, - {}, - self.profile, - callback=self.psItemsSendCb, - errback=partial(self.errback, - msg=_(u"can't send item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Get(base.CommandBase): - - def __init__(self, host): - 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)')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-S", "--sub-id", type=base.unicode_decoder, default=u'', - help=_(u"subscription id")) - # TODO: a key(s) argument to select keys to display - # TODO: add MAM filters - - - def psItemsGetCb(self, ps_result): - self.output(ps_result[0]) - self.host.quit(C.EXIT_OK) - - def psItemsGetEb(self, failure_): - self.disp(u"can't get pubsub items: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psItemsGet( - self.args.service, - self.args.node, - self.args.max, - self.args.items, - self.args.sub_id, - {}, - self.profile, - callback=self.psItemsGetCb, - errback=self.psItemsGetEb) - -class Delete(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'delete an item')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-f", "--force", action='store_true', help=_(u"delete without confirmation")) - self.parser.add_argument("-N", "--notify", action='store_true', help=_(u"notify deletion")) - - def psItemsDeleteCb(self): - self.disp(_(u'item {item_id} has been deleted').format(item_id=self.args.item)) - self.host.quit(C.EXIT_OK) - - def start(self): - if not self.args.item: - self.parser.error(_(u"You need to specify an item to delete")) - if not self.args.force: - message = _(u"Are you sure to delete item {item_id} ?").format(item_id=self.args.item) - self.host.confirmOrQuit(message, _(u"item deletion cancelled")) - self.host.bridge.psRetractItem( - self.args.service, - self.args.node, - self.args.item, - self.args.notify, - self.profile, - callback=self.psItemsDeleteCb, - errback=partial(self.errback, - msg=_(u"can't delete item: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Edit(base.CommandBase, common.BaseEdit): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'edit', use_verbose=True, use_pubsub=True, - pubsub_flags={C.NODE, C.SINGLE_ITEM}, use_draft=True, help=_(u'edit an existing or new pubsub item')) - common.BaseEdit.__init__(self, self.host, PUBSUB_TMP_DIR) - - def add_parser_options(self): - pass - - def edit(self, content_file_path, content_file_obj): - # we launch editor - self.runEditor("pubsub_editor_args", content_file_path, content_file_obj) - - def publish(self, content): - published_id = self.host.bridge.psItemSend(self.pubsub_service, self.pubsub_node, content, self.pubsub_item or '', {}, self.profile) - if published_id: - self.disp(u"Item published at {pub_id}".format(pub_id=published_id)) - else: - self.disp(u"Item published") - - def getItemData(self, service, node, item): - try: - from lxml import etree - except ImportError: - self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True) - self.host.quit(1) - items = [item] if item is not None else [] - item_raw = self.host.bridge.psItemsGet(service, node, 1, items, "", {}, self.profile)[0][0] - parser = etree.XMLParser(remove_blank_text=True) - item_elt = etree.fromstring(item_raw, parser) - item_id = item_elt.get('id') - try: - payload = item_elt[0] - except IndexError: - self.disp(_(u'Item has not payload'), 1) - return u'' - return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id - - def start(self): - self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj = self.getItemPath() - self.edit(content_file_path, content_file_obj) - - -class Subscribe(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'subscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'subscribe to a node')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psSubscribeCb(self, sub_id): - self.disp(_(u'subscription done'), 1) - if sub_id: - self.disp(_(u'subscription id: {sub_id}').format(sub_id=sub_id)) - self.host.quit() - - def start(self): - self.host.bridge.psSubscribe( - self.args.service, - self.args.node, - {}, - self.profile, - callback=self.psSubscribeCb, - errback=partial(self.errback, - msg=_(u"can't subscribe to node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Unsubscribe(base.CommandBase): - # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'unsubscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'unsubscribe from a node')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psUnsubscribeCb(self): - self.disp(_(u'subscription removed'), 1) - self.host.quit() - - def start(self): - self.host.bridge.psUnsubscribe( - self.args.service, - self.args.node, - self.profile, - callback=self.psUnsubscribeCb, - errback=partial(self.errback, - msg=_(u"can't unsubscribe from node: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Subscriptions(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'subscriptions', use_output=C.OUTPUT_LIST_DICT, use_pubsub=True, help=_(u'retrieve all subscriptions on a service')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psSubscriptionsGetCb(self, subscriptions): - self.output(subscriptions) - self.host.quit() - - def start(self): - self.host.bridge.psSubscriptionsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psSubscriptionsGetCb, - errback=partial(self.errback, - msg=_(u"can't retrieve subscriptions: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Affiliations(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'affiliations', use_output=C.OUTPUT_DICT, use_pubsub=True, help=_(u'retrieve all affiliations on a service')) - self.need_loop=True - - def add_parser_options(self): - pass - - def psAffiliationsGetCb(self, affiliations): - self.output(affiliations) - self.host.quit() - - def psAffiliationsGetEb(self, failure_): - self.disp(u"can't get node affiliations: {reason}".format( - reason=failure_), error=True) - self.host.quit(C.EXIT_BRIDGE_ERRBACK) - - def start(self): - self.host.bridge.psAffiliationsGet( - self.args.service, - self.args.node, - self.profile, - callback=self.psAffiliationsGetCb, - errback=self.psAffiliationsGetEb) - - -class Search(base.CommandBase): - """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""" - RE_FLAGS = re.MULTILINE | re.UNICODE - EXEC_ACTIONS = (u'exec', u'external') - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'search', use_output=C.OUTPUT_XML, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS, C.NO_MAX}, - use_verbose=True, help=_(u'search items corresponding to filters')) - self.need_loop=True - - @property - def etree(self): - """load lxml.etree only if needed""" - if self._etree is None: - from lxml import etree - self._etree = etree - return self._etree - - def filter_opt(self, value, type_): - value = base.unicode_decoder(value) - return (type_, value) - - def filter_flag(self, value, type_): - value = C.bool(value) - return (type_, value) - - def add_parser_options(self): - 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)")) - 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))) - self.parser.add_argument("-N", "--namespace", action='append', nargs=2, default=[], - metavar="NAME NAMESPACE", help=_(u"namespace to use for xpath")) - - # filters - filter_text = partial(self.filter_opt, type_=u'text') - filter_re = partial(self.filter_opt, type_=u'regex') - filter_xpath = partial(self.filter_opt, type_=u'xpath') - filter_python = partial(self.filter_opt, type_=u'python') - filters = self.parser.add_argument_group(_(u'filters'), _(u'only items corresponding to following filters will be kept')) - filters.add_argument("-t", "--text", - action='append', dest='filters', type=filter_text, - metavar='TEXT', - help=_(u"full text filter, item must contain this string (XML included)")) - filters.add_argument("-r", "--regex", - action='append', dest='filters', type=filter_re, - metavar='EXPRESSION', - help=_(u"like --text but using a regular expression")) - filters.add_argument("-x", "--xpath", - action='append', dest='filters', type=filter_xpath, - metavar='XPATH', - help=_(u"filter items which has elements matching this xpath")) - filters.add_argument("-P", "--python", - action='append', dest='filters', type=filter_python, - metavar='PYTHON_CODE', - 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')) - - # filters flags - flag_case = partial(self.filter_flag, type_=u'ignore-case') - flag_invert = partial(self.filter_flag, type_=u'invert') - flag_dotall = partial(self.filter_flag, type_=u'dotall') - flag_matching = partial(self.filter_flag, type_=u'only-matching') - flags = self.parser.add_argument_group(_(u'filters flags'), _(u'filters modifiers (change behaviour of following filters)')) - flags.add_argument("-C", "--ignore-case", - action='append', dest='filters', type=flag_case, - const=('ignore-case', True), nargs='?', - metavar='BOOLEAN', - help=_(u"(don't) ignore case in following filters (default: case sensitive)")) - flags.add_argument("-I", "--invert", - action='append', dest='filters', type=flag_invert, - const=('invert', True), nargs='?', - metavar='BOOLEAN', - help=_(u"(don't) invert effect of following filters (default: don't invert)")) - flags.add_argument("-A", "--dot-all", - action='append', dest='filters', type=flag_dotall, - const=('dotall', True), nargs='?', - metavar='BOOLEAN', - help=_(u"(don't) use DOTALL option for regex (default: don't use)")) - flags.add_argument("-o", "--only-matching", - action='append', dest='filters', type=flag_matching, - const=('only-matching', True), nargs='?', - metavar='BOOLEAN', - help=_(u"keep only the matching part of the item")) - - # action - self.parser.add_argument("action", - default="print", - nargs='?', - choices=('print', 'exec', 'external'), - help=_(u"action to do on found items (default: print)")) - self.parser.add_argument("command", nargs=argparse.REMAINDER) - - def psItemsGetEb(self, failure_, service, node): - self.disp(u"can't get pubsub items at {service} (node: {node}): {reason}".format( - service=service, - node=node, - reason=failure_), error=True) - self.to_get -= 1 - - def getItems(self, depth, service, node, items): - search = partial(self.search, depth=depth) - errback = partial(self.psItemsGetEb, service=service, node=node) - self.host.bridge.psItemsGet( - service, - node, - self.args.max, - items, - "", - {}, - self.profile, - callback=search, - errback=errback - ) - self.to_get += 1 - - def _checkPubsubURL(self, match, found_nodes): - """check that the matched URL is an xmpp: one - - @param found_nodes(list[unicode]): found_nodes - this list will be filled while xmpp: URIs are discovered - """ - url = match.group(0) - if url.startswith(u'xmpp'): - try: - url_data = uri.parseXMPPUri(url) - except ValueError: - return - if url_data[u'type'] == u'pubsub': - found_node = {u'service': url_data[u'path'], - u'node': url_data[u'node']} - if u'item' in url_data: - found_node[u'item'] = url_data[u'item'] - found_nodes.append(found_node) - - def getSubNodes(self, item, depth): - """look for pubsub URIs in item, and getItems on the linked nodes""" - found_nodes = [] - checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes) - strings.RE_URL.sub(checkURI, item) - for data in found_nodes: - self.getItems(depth+1, - data[u'service'], - data[u'node'], - [data[u'item']] if u'item' in data else [] - ) - - def parseXml(self, item): - try: - return self.etree.fromstring(item) - except self.etree.XMLSyntaxError: - 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) - self.host.quit(C.EXIT_BAD_ARG) - - def filter(self, item): - """apply filters given on command line - - if only-matching is used, item may be modified - @return (tuple[bool, unicode]): a tuple with: - - keep: True if item passed the filters - - item: it is returned in case of modifications - """ - ignore_case = False - invert = False - dotall = False - only_matching = False - item_xml = None - for type_, value in self.args.filters: - keep = True - - ## filters - - if type_ == u'text': - if ignore_case: - if value.lower() not in item.lower(): - keep = False - else: - if value not in item: - keep = False - if keep and only_matching: - # doesn't really make sens to keep a fixed string - # so we raise an error - self.host.disp(_(u"--only-matching used with fixed --text string, are you sure?"), error=True) - self.host.quit(C.EXIT_BAD_ARG) - elif type_ == u'regex': - flags = self.RE_FLAGS - if ignore_case: - flags |= re.IGNORECASE - if dotall: - flags |= re.DOTALL - match = re.search(value, item, flags) - keep = match != None - if keep and only_matching: - item = match.group() - item_xml = None - elif type_ == u'xpath': - if item_xml is None: - item_xml = self.parseXml(item) - try: - elts = item_xml.xpath(value, namespaces=self.args.namespace) - except self.etree.XPathEvalError as e: - self.disp(_(u"can't use xpath: {reason}").format(reason=e), error=True) - self.host.quit(C.EXIT_BAD_ARG) - keep = bool(elts) - if keep and only_matching: - item_xml = elts[0] - try: - item = self.etree.tostring(item_xml, encoding='unicode') - except TypeError: - # we have a string only, not an element - item = unicode(item_xml) - item_xml = None - elif type_ == u'python': - if item_xml is None: - item_xml = self.parseXml(item) - cmd_ns = {u'item': item, - u'item_xml': item_xml - } - try: - keep = eval(value, cmd_ns) - except SyntaxError as e: - self.disp(unicode(e), error=True) - self.host.quit(C.EXIT_BAD_ARG) - - ## flags - - elif type_ == u'ignore-case': - ignore_case = value - elif type_ == u'invert': - invert = value - # we need to continue, else loop would end here - continue - elif type_ == u'dotall': - dotall = value - elif type_ == u'only-matching': - only_matching = value - else: - raise exceptions.InternalError(_(u"unknown filter type {type}").format(type=type_)) - - if invert: - keep = not keep - if not keep: - return False, item - - return True, item - - def doItemAction(self, item, metadata): - """called when item has been kepts and the action need to be done - - @param item(unicode): accepted item - """ - action = self.args.action - if action == u'print' or self.host.verbosity > 0: - try: - self.output(item) - except self.etree.XMLSyntaxError: - # item is not valid XML, but a string - # can happen when --only-matching is used - self.disp(item) - if action in self.EXEC_ACTIONS: - item_elt = self.parseXml(item) - if action == u'exec': - use = {'service': metadata[u'service'], - 'node': metadata[u'node'], - 'item': item_elt.get('id'), - 'profile': self.profile - } - # we need to send a copy of self.args.command - # else it would be modified - parser_args, use_args = arg_tools.get_use_args(self.host, - self.args.command, - use, - verbose=self.host.verbosity > 1 - ) - cmd_args = sys.argv[0:1] + parser_args + use_args - else: - cmd_args = self.args.command - - - self.disp(u'COMMAND: {command}'.format( - command = u' '.join([arg_tools.escape(a) for a in cmd_args])), 2) - if action == u'exec': - ret = subprocess.call(cmd_args) - else: - p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE) - p.communicate(item.encode('utf-8')) - ret = p.wait() - if ret != 0: - self.disp(A.color(C.A_FAILURE, _(u"executed command failed with exit code {code}").format(code=ret))) - - def search(self, items_data, depth): - """callback of getItems - - this method filters items, get sub nodes if needed, - do the requested action, and exit the command when everything is done - @param items_data(tuple): result of getItems - @param depth(int): current depth level - 0 for first node, 1 for first children, and so on - """ - items, metadata = items_data - for item in items: - if depth < self.args.max_depth: - self.getSubNodes(item, depth) - keep, item = self.filter(item) - if not keep: - continue - self.doItemAction(item, metadata) - - # we check if we got all getItems results - self.to_get -= 1 - if self.to_get == 0: - # yes, we can quit - self.host.quit() - assert self.to_get > 0 - - def start(self): - if self.args.command: - if self.args.action not in self.EXEC_ACTIONS: - self.parser.error(_(u"Command can only be used with {actions} actions").format( - actions=u', '.join(self.EXEC_ACTIONS))) - else: - if self.args.action in self.EXEC_ACTIONS: - self.parser.error(_(u"you need to specify a command to execute")) - if not self.args.node: - # TODO: handle get service affiliations when node is not set - self.parser.error(_(u"empty node is not handled yet")) - # to_get is increased on each get and decreased on each answer - # when it reach 0 again, the command is finished - self.to_get = 0 - self._etree = None - if self.args.filters is None: - self.args.filters = [] - self.args.namespace = dict(self.args.namespace + [('pubsub', "http://jabber.org/protocol/pubsub")]) - self.getItems(0, self.args.service, self.args.node, self.args.items) - - -class Uri(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'uri', use_profile=False, use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'build URI')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument("-p", "--profile", type=base.unicode_decoder, default=C.PROF_KEY_DEFAULT, help=_(u"profile (used when no server is specified)")) - - def display_uri(self, jid_): - uri_args = {} - if not self.args.service: - self.args.service = jid.JID(jid_).bare - - for key in ('node', 'service', 'item'): - value = getattr(self.args, key) - if key == 'service': - key = 'path' - if value: - uri_args[key] = value - self.disp(uri.buildXMPPUri(u'pubsub', **uri_args)) - self.host.quit() - - def start(self): - if not self.args.service: - self.host.bridge.asyncGetParamA( - u'JabberID', - u'Connection', - profile_key=self.args.profile, - callback=self.display_uri, - errback=partial(self.errback, - msg=_(u"can't retrieve jid: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - else: - self.display_uri(None) - - -class HookCreate(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'create a Pubsub hook')) - self.need_loop=True - - def add_parser_options(self): - self.parser.add_argument('-t', '--type', default=u'python', choices=('python', 'python_file', 'python_code'), help=_(u"hook type")) - self.parser.add_argument('-P', '--persistent', action='store_true', help=_(u"make hook persistent across restarts")) - self.parser.add_argument("hook_arg", type=base.unicode_decoder, help=_(u"argument of the hook (depend of the type)")) - - @staticmethod - def checkArgs(self): - if self.args.type == u'python_file': - self.args.hook_arg = os.path.abspath(self.args.hook_arg) - if not os.path.isfile(self.args.hook_arg): - self.parser.error(_(u"{path} is not a file").format(path=self.args.hook_arg)) - - def start(self): - self.checkArgs(self) - self.host.bridge.psHookAdd( - self.args.service, - self.args.node, - self.args.type, - self.args.hook_arg, - self.args.persistent, - self.profile, - callback=self.host.quit, - errback=partial(self.errback, - msg=_(u"can't create hook: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class HookDelete(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a Pubsub hook')) - self.need_loop=True - - def add_parser_options(self): - 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)")) - 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)")) - - def psHookRemoveCb(self, nb_deleted): - self.disp(_(u'{nb_deleted} hook(s) have been deleted').format( - nb_deleted = nb_deleted)) - self.host.quit() - - def start(self): - HookCreate.checkArgs(self) - self.host.bridge.psHookRemove( - self.args.service, - self.args.node, - self.args.type, - self.args.hook_arg, - self.profile, - callback=self.psHookRemoveCb, - errback=partial(self.errback, - msg=_(u"can't delete hook: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class HookList(base.CommandBase): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'list', use_output=C.OUTPUT_LIST_DICT, help=_(u'list hooks of a profile')) - self.need_loop = True - - def add_parser_options(self): - pass - - def psHookListCb(self, data): - if not data: - self.disp(_(u'No hook found.')) - self.output(data) - self.host.quit() - - def start(self): - self.host.bridge.psHookList( - self.profile, - callback=self.psHookListCb, - errback=partial(self.errback, - msg=_(u"can't list hooks: {}"), - exit_code=C.EXIT_BRIDGE_ERRBACK)) - - -class Hook(base.CommandBase): - subcommands = (HookCreate, HookDelete, HookList) - - def __init__(self, host): - super(Hook, self).__init__(host, 'hook', use_profile=False, use_verbose=True, help=_('trigger action on Pubsub notifications')) - - -class Pubsub(base.CommandBase): - subcommands = (Set, Get, Delete, Edit, Subscribe, Unsubscribe, Subscriptions, Node, Affiliations, Search, Hook, Uri) - - def __init__(self, host): - super(Pubsub, self).__init__(host, 'pubsub', use_profile=False, help=_('PubSub nodes/items management'))
--- a/frontends/src/jp/cmd_roster.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,214 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SAT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) -# Copyright (C) 2003-2016 Adrien Cossa (souliane@mailoo.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import base -from sat_frontends.jp.constants import Const as C -from sat.core.i18n import _ - -from twisted.words.protocols.jabber import jid -from collections import OrderedDict - -__commands__ = ["Roster"] - - - -class Purge(base.CommandBase): - - def __init__(self, host): - super(Purge, self).__init__(host, 'purge', help=_('Purge the roster from its contacts with no subscription')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument("--no_from", action="store_true", help=_("Also purge contacts with no 'from' subscription")) - self.parser.add_argument("--no_to", action="store_true", help=_("Also purge contacts with no 'to' subscription")) - - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) - - def error(self, failure): - print (_("Error while retrieving the contacts [%s]") % failure) - self.host.quit(1) - - def ask_confirmation(self, no_sub, no_from, no_to): - """Ask the confirmation before removing contacts. - - @param no_sub (list[unicode]): list of contacts with no subscription - @param no_from (list[unicode]): list of contacts with no 'from' subscription - @param no_to (list[unicode]): list of contacts with no 'to' subscription - @return bool - """ - if no_sub: - print "There's no subscription between profile [%s] and the following contacts:" % self.host.profile - print " " + "\n ".join(no_sub) - if no_from: - print "There's no 'from' subscription between profile [%s] and the following contacts:" % self.host.profile - print " " + "\n ".join(no_from) - if no_to: - print "There's no 'to' subscription between profile [%s] and the following contacts:" % self.host.profile - print " " + "\n ".join(no_to) - message = "REMOVE them from profile [%s]'s roster" % self.host.profile - while True: - res = raw_input("%s (y/N)? " % message) - if not res or res.lower() == 'n': - return False - if res.lower() == 'y': - return True - - def gotContacts(self, contacts): - """Process the list of contacts. - - @param contacts(list[tuple]): list of contacts with their attributes and groups - """ - no_sub, no_from, no_to = [], [], [] - for contact, attrs, groups in contacts: - from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) - if not from_: - if not to: - no_sub.append(contact) - elif self.args.no_from: - no_from.append(contact) - elif not to and self.args.no_to: - no_to.append(contact) - if not no_sub and not no_from and not no_to: - print "Nothing to do - there's a from and/or to subscription(s) between profile [%s] and each of its contacts" % self.host.profile - elif self.ask_confirmation(no_sub, no_from, no_to): - for contact in no_sub + no_from + no_to: - self.host.bridge.delContact(contact, profile_key=self.host.profile, callback=lambda dummy: None, errback=lambda failure: None) - self.host.quit() - - -class Stats(base.CommandBase): - - def __init__(self, host): - super(Stats, self).__init__(host, 'stats', help=_('Show statistics about a roster')) - self.need_loop = True - - def add_parser_options(self): - pass - - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) - - def error(self, failure): - print (_("Error while retrieving the contacts [%s]") % failure) - self.host.quit(1) - - def gotContacts(self, contacts): - """Process the list of contacts. - - @param contacts(list[tuple]): list of contacts with their attributes and groups - """ - hosts = {} - unique_groups = set() - no_sub, no_from, no_to, no_group, total_group_subscription = 0, 0, 0, 0, 0 - for contact, attrs, groups in contacts: - from_, to = C.bool(attrs["from"]), C.bool(attrs["to"]) - if not from_: - if not to: - no_sub += 1 - else: - no_from += 1 - elif not to: - no_to += 1 - host = jid.JID(contact).host - hosts.setdefault(host, 0) - hosts[host] += 1 - if groups: - unique_groups.update(groups) - total_group_subscription += len(groups) - if not groups: - no_group += 1 - hosts = OrderedDict(sorted(hosts.items(), key=lambda item:-item[1])) - - print - print "Total number of contacts: %d" % len(contacts) - print "Number of different hosts: %d" % len(hosts) - print - for host, count in hosts.iteritems(): - print "Contacts on {host}: {count} ({rate:.1f}%)".format(host=host, count=count, rate=100 * float(count) / len(contacts)) - print - print "Contacts with no 'from' subscription: %d" % no_from - print "Contacts with no 'to' subscription: %d" % no_to - print "Contacts with no subscription at all: %d" % no_sub - print - print "Total number of groups: %d" % len(unique_groups) - try: - contacts_per_group = float(total_group_subscription) / len(unique_groups) - except ZeroDivisionError: - contacts_per_group = 0 - print "Average contacts per group: {:.1f}".format(contacts_per_group) - try: - groups_per_contact = float(total_group_subscription) / len(contacts) - except ZeroDivisionError: - groups_per_contact = 0 - print "Average groups' subscriptions per contact: {:.1f}".format(groups_per_contact) - print "Contacts not assigned to any group: %d" % no_group - self.host.quit() - - -class Get(base.CommandBase): - - def __init__(self, host): - super(Get, self).__init__(host, 'get', help=_('Retrieve the roster contacts')) - self.need_loop = True - - def add_parser_options(self): - self.parser.add_argument("--subscriptions", action="store_true", help=_("Show the contacts' subscriptions")) - self.parser.add_argument("--groups", action="store_true", help=_("Show the contacts' groups")) - self.parser.add_argument("--name", action="store_true", help=_("Show the contacts' names")) - - def start(self): - self.host.bridge.getContacts(profile_key=self.host.profile, callback=self.gotContacts, errback=self.error) - - def error(self, failure): - print (_("Error while retrieving the contacts [%s]") % failure) - self.host.quit(1) - - def gotContacts(self, contacts): - """Process the list of contacts. - - @param contacts(list[tuple]): list of contacts with their attributes and groups - """ - field_count = 1 # only display the contact by default - if self.args.subscriptions: - field_count += 3 # ask, from, to - if self.args.name: - field_count += 1 - if self.args.groups: - field_count += 1 - for contact, attrs, groups in contacts: - args = [contact] - if self.args.subscriptions: - args.append("ask" if C.bool(attrs["ask"]) else "") - args.append("from" if C.bool(attrs["from"]) else "") - args.append("to" if C.bool(attrs["to"]) else "") - if self.args.name: - args.append(unicode(attrs.get("name", ""))) - if self.args.groups: - args.append(u"\t".join(groups) if groups else "") - print u";".join(["{}"] * field_count).format(*args).encode("utf-8") - self.host.quit() - - -class Roster(base.CommandBase): - subcommands = (Get, Stats, Purge) - - def __init__(self, host): - super(Roster, self).__init__(host, 'roster', use_profile=True, help=_("Manage an entity's roster"))
--- a/frontends/src/jp/cmd_shell.py Mon Apr 02 08:56:24 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,264 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# jp: a SàT command line tool -# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - - -import base -import cmd -import sys -from sat.core.i18n import _ -from sat.core import exceptions -from sat_frontends.jp.constants import Const as C -from sat_frontends.jp import arg_tools -from sat.tools.common.ansi import ANSI as A -import shlex -import subprocess - -__commands__ = ["Shell"] -INTRO = _(u"""Welcome to {app_name} shell, the Salut à Toi shell ! - -This enrironment helps you using several {app_name} commands with similar parameters. - -To quit, just enter "quit" or press C-d. -Enter "help" or "?" to know what to do -""").format(app_name = C.APP_NAME) - - -class Shell(base.CommandBase, cmd.Cmd): - - def __init__(self, host): - base.CommandBase.__init__(self, host, 'shell', help=_(u'launch jp in shell (REPL) mode')) - cmd.Cmd.__init__(self) - - def parse_args(self, args): - """parse line arguments""" - return shlex.split(args, posix=True) - - def update_path(self): - self._cur_parser = self.host.parser - self.help = u'' - for idx, path_elt in enumerate(self.path): - try: - self._cur_parser = arg_tools.get_cmd_choices(path_elt, self._cur_parser) - except exceptions.NotFound: - self.disp(_(u'bad command path'), error=True) - self.path=self.path[:idx] - break - else: - self.help = self._cur_parser - - self.prompt = A.color(C.A_PROMPT_PATH, u'/'.join(self.path)) + A.color(C.A_PROMPT_SUF, u'> ') - try: - self.actions = arg_tools.get_cmd_choices(parser=self._cur_parser).keys() - except exceptions.NotFound: - self.actions = [] - - def add_parser_options(self): - pass - - def format_args(self, args): - """format argument to be printed with quotes if needed""" - for arg in args: - if " " in arg: - yield arg_tools.escape(arg) - else: - yield arg - - def run_cmd(self, args, external=False): - """run command and retur exit code - - @param args[list[string]]: arguments of the command - must not include program name - @param external(bool): True if it's an external command (i.e. not jp) - @return (int): exit code (0 success, any other int failure) - """ - # FIXME: we have to use subprocess - # and relaunch whole python for now - # because if host.quit() is called in D-Bus callback - # GLib quit the whole app without possibility to stop it - # didn't found a nice way to work around it so far - # Situation should be better when we'll move away from python-dbus - if self.verbose: - self.disp(_(u"COMMAND {external}=> {args}").format( - external = _(u'(external) ') if external else u'', - args=u' '.join(self.format_args(args)))) - if not external: - args = sys.argv[0:1] + args - ret_code= subprocess.call(args) - # XXX: below is a way to launch the command without creating a new process - # may be used when a solution to the aforementioned issue is there - # try: - # self.host.run(args) - # except SystemExit as e: - # ret_code = e.code - # except Exception as e: - # self.disp(A.color(C.A_FAILURE, u'command failed with an exception: {msg}'.format(msg=e)), error=True) - # ret_code = 1 - # else: - # ret_code = 0 - - if ret_code != 0: - self.disp(A.color(C.A_FAILURE, u'command failed with an error code of {err_no}'.format(err_no=ret_code)), error=True) - return ret_code - - def default(self, args): - """called when no shell command is recognized - - will launch the command with args on the line - (i.e. will launch do [args]) - """ - if args=='EOF': - self.do_quit('') - self.do_do(args) - - def do_help(self, args): - """show help message""" - if not args: - self.disp(A.color(C.A_HEADER, _(u'Shell commands:')), no_lf=True) - super(Shell, self).do_help(args) - if not args: - self.disp(A.color(C.A_HEADER, _(u'Action commands:'))) - help_list = self._cur_parser.format_help().split('\n\n') - print('\n\n'.join(help_list[1 if self.path else 2:])) - - def do_debug(self, args): - """launch internal debugger""" - try: - import ipdb as pdb - except ImportError: - import pdb - pdb.set_trace() - - def do_verbose(self, args): - """show verbose mode, or (de)activate it""" - args = self.parse_args(args) - if args: - self.verbose = C.bool(args[0]) - self.disp(_(u'verbose mode is {status}').format( - status = _(u'ENABLED') if self.verbose else _(u'DISABLED'))) - - def do_cmd(self, args): - """change command path""" - if args == '..': - self.path = self.path[:-1] - else: - if not args or args[0] == '/': - self.path = [] - args = '/'.join(args.split()) - for path_elt in args.split('/'): - path_elt = path_elt.strip() - if not path_elt: - continue - self.path.append(path_elt) - self.update_path() - - def do_version(self, args): - """show current SàT/jp version""" - try: - self.host.run(['--version']) - except SystemExit: - pass - - def do_shell(self, args): - """launch an external command (you can use ![command] too)""" - args = self.parse_args(args) - self.run_cmd(args, external=True) - - def do_do(self, args): - """lauch a command""" - args = self.parse_args(args) - if (self._not_default_profile and - not '-p' in args and - not '--profile' in args and - not 'profile' in self.use): - # profile is not specified and we are not using the default profile - # so we need to add it in arguments to use current user profile - if self.verbose: - self.disp(_(u'arg profile={profile} (logged profile)').format(profile=self.profile)) - use = self.use.copy() - use['profile'] = self.profile - else: - use = self.use - - - # args may be modified by use_args - # to remove subparsers from it - parser_args, use_args = arg_tools.get_use_args(self.host, - args, - use, - verbose=self.verbose, - parser=self._cur_parser - ) - cmd_args = self.path + parser_args + use_args - self.run_cmd(cmd_args) - - def do_use(self, args): - """fix an argument""" - args = self.parse_args(args) - if not args: - if not self.use: - self.disp(_(u'no argument in USE')) - else: - self.disp(_(u'arguments in USE:')) - for arg, value in self.use.iteritems(): - self.disp(_(A.color(C.A_SUBHEADER, arg, A.RESET, u' = ', arg_tools.escape(value)))) - elif len(args) != 2: - self.disp(u'bad syntax, please use:\nuse [arg] [value]', error=True) - else: - self.use[args[0]] = u' '.join(args[1:]) - if self.verbose: - self.disp('set {name} = {value}'.format( - name = args[0], value=arg_tools.escape(args[1]))) - - def do_use_clear(self